Skip to content

feat(map): Stage 2, evidence pins and an always-run merge gate - #1057

Merged
cryptskii merged 4 commits into
mainfrom
feat/code-map-merge-gate
Sep 29, 2026
Merged

cryptskii merged 4 commits into
mainfrom
feat/code-map-merge-gate

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Stage 2 of the code map: evidence pins on the intent manifest, a strict per-row --repin, and a merge gate that runs on every PR. This is one PR, built in gated chunks. It stays a draft until the last chunk lands.

Owner rulings (2026-09-29)

  • Pin storage. Pins live in specs/requirements/INTENT_PINS.tsv, keyed by (requirement, symbol, artifact). The manifest stays pure intent; the pins file is the approved evidence snapshot.
  • What a pin stores. Each pin stores its facts explicitly:
    • the §8 status and the outcome;
    • the symbol, artifact, reachability and root;
    • the evidence set and each test's closure;
    • the citation and the production closure;
    • the manifest-row hash, tested_at, and the digest over all of them.
  • Which rows. Every requirement row is pinned, known holes included. Exception rows (-) carry no pin.
  • No reruns at the gate. The gate reruns no tests. A pin binds the production closure and every evidence test's closure, so any change stales the row, and --repin requires the named tests to pass on the current commit.
  • Bootstrap. A one-time CI bootstrap, --pin-unpinned, creates pins only where none exist. It uses CI's map and full board log of one commit. After it, only --pin KEY and --repin KEY run, one key at a time.
  • Repin. Plain --repin accepts code refreshes only. Any semantic change needs --accept <class>: symbol, artifact, reachability or lifecycle, root, status, outcome, evidence, citation, or manifest row.
  • The gate. It runs on every PR with no path filter; today **/*.md PRs skip the map entirely. Branch protection comes only after owner approval.

Chunk 1: closures bind generated code to what generates it

Pins rest on closure hashes. A workspace symbol the index has no definition of, such as prost output or a macro's items, was hashed by name only. So a .proto, build-script or macro change left every closure reaching it unchanged.

Now such a symbol is bound to:

  • its crate's build.rs;
  • the nearest module file its path names;
  • every .proto file;
  • every macro definition.

Nothing else in its package is bound, so unrelated edits don't stale pins. All 2,181 such symbols resolve to a module file. The map refuses a symbol that does not, and a package whose definitions sit under two source directories.

  • Test: generated_code_is_bound_to_what_generates_it.
  • Mutation: with the .proto files dropped from the binding, it goes red on that assertion.
  • The rebuilt map keeps every committed fact: check 0 failures, intent unchanged, 40 of 40 mutation cases, make lint 0, requirement_map 98 of 98.

Next chunks

  1. INTENT_PINS.tsv, pin facts, PIN_STALE and orphan or tamper checks in ci/intent_comparator.py, with a Rust digest under DSM/code-row-evidence/v1.
  2. --pin KEY and --repin KEY [--accept ...], with board-log and tested-at checks.
  3. --pin-unpinned bootstrap from CI artifacts.
  4. An always-run gate workflow.

Stage 2 pins will rest on the map's closure hashes: a row's production
closure and the closure of each test its evidence names. A closure must
change whenever anything its code depends on changes, or a pin would stay
fresh over changed behaviour.

It did not. A workspace symbol the index has no definition of was hashed by
its name alone:
- prost output such as `types/proto/...` and `generated/prost_generated/...`;
- a macro's items, such as `impl#[NodeId]new()`.

Of the 2,181 such symbols, nearly all are prost types. A `.proto` edit, a
build-script change or a macro change would leave every closure that reaches
them unchanged.

Now such a symbol is hashed under `DSM/code-generated/v1` with its symbol and
the digest of what generates it (`DSM/code-generation-inputs/v1`):
- its package's `build.rs`;
- the module file its path names, or the nearest fingerprinted one enclosing
  it (where its `include!` or macro invocation sits);
- every `.proto` file;
- every macro definition.

Nothing else in its package is bound, so a change elsewhere leaves it alone
and does not stale every pin that touches a prost type. Every one of the 2,181
resolves to a module file. The map refuses:
- one that does not;
- a package whose definitions sit under two source directories.

The fixture binds its own crate's files the same way.

Test: generated_code_is_bound_to_what_generates_it.
- The hash changes with the build script, the module file, a `.proto` file
  or a macro definition.
- It does not change with an unrelated file or `lib.rs`.
- Another package's symbol is not generated code.
- A symbol no module file encloses stops the map.

Mutation: with the `.proto` files dropped from the binding, the test goes red
on exactly that assertion.

The rebuilt map keeps every committed fact: sentinels, entry points, counts
and the intent outcomes.
…epin

The owner's Stage 2 rulings (2026-09-29).

What a pin is.
- ci/intent_pins.py keeps specs/requirements/INTENT_PINS.tsv beside the
  manifest. The manifest says what should be true; each pin is the evidence
  snapshot someone verified for one (requirement, symbol, artifact).
- A pin stores its facts explicitly:
  - the manifest cells, and their digest (`DSM/intent-manifest-row/v1`);
  - the §8 status, the comparator's outcome and the map's reading;
  - the definition the path resolves to, and its closure;
  - the closure of every test its evidence names;
  - the commit that evidence passed at;
  - the digest over all of it (`DSM/code-row-evidence/v1`).
- Every requirement row is pinned, known holes included. `-` rows are not.

Check. `ci/intent_comparator.py --pins` reads every pin, after the outcomes,
and fails a row whose:
- facts moved (PIN_STALE), naming each class with the fact's old and new
  value;
- stored digests do not match its facts (PIN_TAMPERED);
- requirement row has no pin (UNPINNED);
- key the manifest no longer has (ORPHAN_PIN).

Pins are read only over a map of every build, because a closure is taken over
every build's edges. On a host that cannot index the storage node, the
comparator reports "not checked" and fails, rather than reading every pin
wrongly.

The gate reruns no test. A pin binds the production closure and the evidence
tests' closures, so a change to either stales the row, and only a board run
on the changed tree refreshes it.

Commands (one key at a time):
- `pin KEY` creates a pin.
- `repin KEY` accepts a code refresh. Every other class that moved must be
  named with `--accept`: symbol, reading, outcome, status, root,
  reachability, evidence, citation or exception. An `--accept` for a class
  that did not move is refused.
- `unpin KEY` removes only a pin whose key has left the manifest.
- `pin-unpinned` is the one-time bootstrap: it creates pins only where none
  exist and never touches one that does.

Before any pin is written, each command refuses unless:
- the map is of this tree (its recorded fingerprint, recomputed);
- this tree's code is the tested commit's (`conformance_evidence.code_at`,
  lifted out of its `main`);
- the map resolves every evidence test to one definition (a test it cannot
  read is `absent:NAME` or `ambiguous:NAME`, a moved fact to the check);
- every evidence test ran and passed in the given board logs.

`intent_comparator.evaluate` returns a named `Evaluated` for both scripts.
A refused pin check goes to stderr and fails the run.

Tests: ci/test_intent_pins.py (11), over the fixture's map.
- Each state is planted in copies of the fixture's manifest, requirements and
  pins, and asserted with its class.
- Also covered: the repin and bootstrap rules, board evidence (ok, failed,
  crashed, not run, no such test), and test names (library, and integration
  `crate::stem::name` over this repository's map).
- Mutations: with tamper detection removed, a hand edit reads as stale; with
  `status` made implicit, the repin refusal goes. Both go red.

rules.tsv gains a `pin` kind. rules.rs reads PIN_STATES from Python and checks
that each state's unit tests exist in ci/test_intent_pins.py. Dropping a
state's row fails.

The requirement_map binary gains `digest --domain manifest-row|row-evidence`,
so every pin hash is a DSM domain-separated BLAKE3. Test:
each_line_digests_its_cells_under_its_domain.

Makefile and CI:
- `requirement-map-intent` passes `--pins`.
- `requirement-map-pin-tests` runs the tests in the Code map job.
- The code-map artifact now carries the tree record, the source and input
  lists, the analyzer version and pins.tsv, so pins can be taken from CI's
  map.

Until the bootstrap chunk commits INTENT_PINS.tsv, the intent step fails with
"not checked: … does not exist". That is the truthful state.
Comment thread ci/test_intent_pins.py Fixed
…s evidence

The merge gate moves out of ci.yml into its own workflow,
.github/workflows/code-map.yml. It runs on every pull request, every push to
main, and by hand. It has no path filter: ci.yml ignores markdown-only PRs,
yet a CONFORMANCE §8 edit alone moves a row's status and so its pin. Its
jobs have no layer condition either. A required check skipped by a path
filter would read as passed.

Job "Code map": the map, its check, the intent manifest with every pin
(INTENT_BUILT=android,node), the pins' tests and the mutation cases, moved
unchanged from ci.yml.

Job "Pin evidence": the half of the gate that runs tests.
- `ci/intent_pins.py evidence --base REF` runs, on this commit with
  Postgres, the evidence tests of every requirement row whose pin is new,
  changed or missing against the base. The base is the PR's base, or the
  previous main.
- It runs exactly those tests (`cargo test -p CRATE --lib|--test STEM|--bins
  -- --exact`, the board's flags). It fails on any non-zero cargo exit and on
  any test that did not pass.
- So a pin is evidence CI proved, never a log only its author saw.
- Where no pins are committed yet, it runs every row's evidence, and that log
  is what the one-time bootstrap pins from.
- Checked end to end locally: a one-row run compiled and ran exactly its
  test and passed.

Found and fixed on the way (gate rounds 5 to 8):
- `pins_at` reads a base's pins only once the ref is a commit and the path
  is listed there. Every other git failure is refused with git's message,
  never read as "no pins".
- `conformance_evidence.code_at` reads `git diff --quiet` exit 1 as a
  difference and refuses any other failure.
- The evidence test reads real captured board logs (ci/fixtures/board-logs):
  a pass, a run whose filter matched nothing, a node-backed test failing
  without its database, and a run killed mid-test. Each is a whole
  `cargo test` run with the gate's flags.
- The generated-code binding test and the digest test read real files and a
  real symbol: the fixture crate's files, the one generated symbol its index
  holds, the repository's `.proto`, and the fixture manifest's rows. The
  proto mutation still turns the binding test red on `proto/dsm_app.proto`.

Tests:
- ci/test_intent_pins.py: 14, including the rows the gate verifies, the exact
  cargo runs, and base pins read from history.
- `cargo test -p requirement_map`: 99.
- `make lint`: 0.
- scripts/check-spdx.sh: OK.
From review (github-code-quality): `row_line` mixed an explicit return with
a fall-through. It never actually returned None, because the fall-through
path called `self.fail`, which raises, but the shape read as one that could.

It now collects every manifest line with the key, asserts there is exactly
one, and returns it. That is stricter than before: a key naming two rows now
fails instead of returning the first.

ci/test_intent_pins.py: 14/14.
@cryptskii
cryptskii marked this pull request as ready for review September 29, 2026 10:34
@cryptskii
cryptskii merged commit 8b28149 into main Sep 29, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant