Files
inliver233 6ab905b2c8 Four-state labels, and the negatives that used to look alike
Every claim carried one of three labels, and the third was doing two jobs. `unverified` meant
both "someone asserted it and nobody here paid for the counters" and "no evidence was gathered
at all" -- different statements with different next actions. This adds `unknown` for the second.

The distinction that pays for itself lives inside a *negative* result, because an empty probe has
three causes and only one of them is a finding:

  absent                  -- coverage reached the question, nothing is there
  present but unreadable  -- the thing is there and this route cannot read it
  unknown                 -- nothing covered the question

The middle one is the dangerous one: it produces byte-identical output to the first, and it is the
normal case on the input this skill is aimed at. A packed APK returning four classes is not an APK
with four classes, and "the string was not in the dex" is a statement about a probe. The rule now
says to name the probe's coverage and run a control that must hit before writing "not present", and
to record `unknown` about the probe rather than `absent` about the target.

Also in this pass:

  * P47 -- two tools disagree about the same artifact, and the disagreement is your parser. One
    extraction rule reused across outputs that do not share a format: an unescaped `$` dropped every
    descriptor containing one, and dotted names were compared against DEX descriptors so one side
    parsed to nothing. The comfortable failure is the one that confirms what you already expected,
    which is why it survived the sanity check it should not have.
  * dexsql: a cache is second-hand evidence and it ages silently. A stale cache answers for the old
    artifact with no error, and `--trusted-input` skips revalidating the recorded source inputs --
    so a cached run supports "true of the artifact the cache was built from", not "true of the
    artifact in front of me".
  * ete-diff-tracing: a divergence is a location, not a cause. The differential method produces a
    positional claim, and three readings it does not authorise are written down beside the step that
    produces it.
  * CONTRIBUTING.md -- the gates, what a green run does and does not establish, the sync rules a
    change has to keep (a new reference must be named in the inventory; renaming a heading breaks
    every section reference that pointed at it), and the two failure modes to watch when editing the
    gates: a limit that cannot fail, and a gate whose answer varies with the runner. README links it
    from both language editions.
2026-10-16 19:17:31 +08:00

108 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Contributing
This repository is a skill plus the evidence behind it. Two properties keep it maintainable by more
than one person, and both are **enforced by scripts rather than requested in prose**:
- **every claim carries a strength label**, and
- **every structural relationship is checked** — paths resolve, the on-demand inventory matches the
entry point, documented commands exist, the always-loaded part has not crept.
The checks exist because both properties decay silently. A stale index still reads as complete; a
claim written from expectation reads like a measurement. If you change something and a gate goes red,
the gate is usually right.
## Run the gates before you push
```bash
python check_repo.py && python check_refs.py && python check_routing.py \
&& python check_commands.py && python check_budget.py && python -m pytest -q
```
Nothing above needs a device, a sample APK, or the network. CI runs the same set on Python 3.11–3.14
plus `mypy` and `flake8`, so a green local run is what a green CI run means.
If `flake8`/`mypy` are not installed, that is the one part CI checks that you cannot reproduce
locally; everything else is plain Python.
## What each gate owns
| Gate | Owns | It cannot see |
|---|---|---|
| `check_repo.py` | frontmatter parses, scripts answer `--help`, every referenced path resolves, every reference is reachable, README paths exist, no target identity on the tracked surface | whether a claim is true |
| `check_refs.py` | every section reference reaches a real heading in the file it names | whether the section still says what the link implies |
| `check_routing.py` | `references/routing.md` and `SKILL.md` agree: symptom mirror identical, every reference file named, every script named | semantics — you can name a file in the wrong row |
| `check_commands.py` | every `python scripts/x.py --flag` a document tells a reader to run matches the script's argparse table | commands inside fenced blocks it cannot attribute to a script |
| `check_budget.py` | `SKILL.md` size in lines and tokens, index-row length, long files with a navigable head | whether a new file is load-bearing or merely adjacent |
**A green run proves consistency, not correctness.** It says paths resolve and the inventory is
complete. It does not say a mechanism works, a tool behaves as described on your device, or a fixture
stands in for a real target. Only the strength labels and the evidence record answer those.
## What a change has to keep in step
This is the list that costs a contributor a red CI run if they miss it:
| If you add or change | Also do | Caught by |
|---|---|---|
| a reference file | name it in `references/routing.md`; make it reachable from `SKILL.md` or the README; give it a "load this when" head if it passes 100 lines | `check_routing.py`, `check_repo.py`, `check_budget.py` |
| a script | add a row to the script table in `references/routing.md`; declare its capability in `scripts/capabilities.py` | `check_routing.py`, `check_repo.py` |
| a symptom-index row | mirror it in `routing.md` (the mirror is compared, not generated) | `check_routing.py` |
| a section heading | update every section reference that pointed at the old name | `check_refs.py` |
| a command in a document | keep the flags real — copy them from `--help`, do not retype them | `check_commands.py` |
| `SKILL.md` | watch the budget: it is loaded in full on every activation, index rows included | `check_budget.py` |
| a capability claim | add or update the row in `evidence/capability-matrix.json` **and** the line in `references/evidence-summary.md` | review — no gate compares them |
| a new pitfall | next number, same four headings (`Symptom` / `Root cause` / `Why it is hard to see` / `The rule`) | review |
The last two rows are deliberately unenforced. A gate can check that a file is named; it cannot check
that a claim is honest, which is why those two are the ones a reviewer has to read.
## The label discipline
Every claim in this repository carries one of four labels (`SKILL.md` §Coverage defines them and
`references/coverage-and-limits.md` carries the long form): `observed`, `inferred`, `unverified`,
`unknown`. Two rules matter more than the rest:
- **`observed` requires a command and its output.** "I reasoned it out" is `inferred`; "someone said
so" is `unverified`; "nobody looked" is `unknown`.
- **A negative result is `absent`, `unknown` or `present but unreadable`, and only the first is a
finding.** A probe that came back empty because it could not read the input looks identical to one
that came back empty because the thing is not there. That confusion is *normal* on hardened targets,
so name the probe's coverage and run a control that must hit before writing anything down as
"not there".
If you measured something on one version, one device or one ABI, say which. A number without its
scope is the most common way a correct measurement becomes a wrong claim.
## Pull requests
- **Say what you ran.** Paste the command and the output that matters, not a summary of your
confidence. Report only checks that actually executed; "should pass" is not a result.
- **Label your own claims** in the same four labels. A large change whose assumptions are labelled is
easier to review than a small one that presents them as findings.
- **Keep the gate output in the PR body** when a gate was the reason for a change. It is the cheapest
evidence a reviewer can check.
- **Do not treat a green CI run as approval.** It establishes that code runs and paths resolve — not
that a claim is true, not that a limit is honest, and not that the change belongs here.
- **Prefer a narrow change with a stated boundary** over a broad one with an implied guarantee. Where
something could not be exercised here, ship the measured half plus an explicitly labelled template
for the rest, and say what would have to exist for the other half to become `observed`.
- **One variable at a time.** If a result changed, it should be attributable to the one thing you
changed (`references/long-task-discipline.md` §single-variable discipline).
## Changing the gates themselves
The gates are code and they can be wrong in two directions: blind (a defect they should catch passes)
and noisy (they fail on a clean tree). Both are testable, and this repository has been bitten by both:
- **A limit that cannot fail is decorative.** `check_budget.py` once computed its hard index-row limit
and reported `warn` on both branches, so an index row could grow without bound while CI stayed
green. Watch for the shape where a threshold is defined, evaluated and never escalates.
- **A gate that only varies with the runner is not a gate.** `check_budget.py`'s token estimate had a
guessed constant, so CI (no `tiktoken`) and a developer machine reached different verdicts.
- **The way to find the first class is to inject the defect and require the failure.** Add the case to
`tests/unit/` so the next person cannot re-break it: `test_budget_measurement.py` pins both
behaviours above.
When you fix a gate, add the test that would have caught it, and say in the PR which defect the test
was written against.