Skip to content

feat: evidence bound to its inputs, gates that fire, and a second reviewer (ADR-017 to ADR-022) - #9

Merged
sscodeai merged 20 commits into
devfrom
feat/evidence-gates-and-mirrors
Sep 30, 2026
Merged

sscodeai merged 20 commits into
devfrom
feat/evidence-gates-and-mirrors

Conversation

@sscodeai

Copy link
Copy Markdown
Owner

Carries this block into dev. The first PR for this block was opened against the previous branch of the stack, so it merged there; same commits, no content change.

sscodeai and others added 20 commits September 28, 2026 09:30
Found by wiring MirroringBoard to a caller for the first time (the ADR-017 slice). `resync` of a real
item failed: `projected 1, failed 1`, with the mirror saying

  state pr_open not projected onto mirror-fake: illegal board transition ready -> pr_open:
  allowed from ready: claimed

Every mirror item was created at `ready` and then WALKED to the state being projected. That walk does
not exist — the state table allows `ready -> claimed` and nothing else out of `ready` — so a projection
of any item past `claimed` (pr_open, fix_round, merged) could never be built, and `resync`, whose whole
promise is "a projection is rebuildable", could not rebuild in-flight work at all.

The port already had the answer: `BoardWorkItemSpec.state` ("the state the new item starts in, default
ready"), honoured by all six adapters. The mirror simply never passed it.

- the mirror item is created IN the projected state, so no walk is needed
- a comment arriving FIRST for an item the mirror has never seen reads the state from the authority
  rather than assuming `ready` (creating the copy at `ready` would state something false about work
  that is already merged)
- the test double stopped hardcoding `state: 'ready'` and now honours `spec.state` like every real
  adapter — that hardcoding is why core's suite never saw this: the double answered more kindly than
  the host (the ledger's class list, #13), and only a real caller could expose it

Verified with a real resync over items in ready/pr_open/merged: projected 3, failed 0, and the mirror's
items read ready/pr_open/merged. Core 244/241/0, CLI 47/47.
Fix is 9bc6f90. Found the first time the decorator had a caller — which is the argument for wiring a
capability in the same slice that builds it, and for a test double that answers exactly as unkindly as
the host.
… (`pilot --resync`)

ADR-017 built MirroringBoard and never had a caller: the "capability with no reader" smell this project
keeps removing. This is the wiring, and wiring it found a real bug (fix `9bc6f90`, ledger #21).

- `pilot.boardMirrors: [{ id, provider, options, idMapFile }]` in takumi.yaml — the board OTHER people
  look at, built through the same provider factory `takumi board` uses, so a mirror can never be a
  configuration the CLI cannot also express or check
- the composite is constructed where the board is built, with the event log, so mirror writes and
  failures are EVENTS (`mirror.written` / `mirror.failed`) rather than whatever the mirror felt like
- what is projected WHERE is printed on every tick (`mirror   notion: canCreateWork=true,
  editableComment=false`): the check ADR-017 asks for, and the reason a read-only projection is never
  mistaken for an authority
- `takumi pilot --resync`: rebuild every projection from the primary WITHOUT running a tick, then save
  the id map. Asked for with no mirrors configured, it says so instead of printing nothing.
- the id map is persisted next to the slot dir: loaded tolerantly (a damaged cache costs API calls, not
  correctness), written after the work, and a write failure is REPORTED rather than thrown — a cache
  must not be able to fail a tick that already happened
- the primary's `pr_open` note now carries the MR/PR URL: "PR #7" without a link is a reference a reader
  (in particular a mirror's reader) cannot follow

Tests drive the COMMAND, not the class: a configured mirror is projected onto and its id map lands on
disk, `--resync` rebuilds without a tick, and `--resync` with no mirrors is an error rather than a
silence. Core 244/241/0, CLI 47/47, 0 TS errors.

Live Notion check next, pending a token: the database already exists with the right property names and
the seven states as select options.
Found by projecting real work into a real Notion database (ADR-017): `resync` reported
`projected 1, failed 0` where the GitLab project holds seven items. Five of them were merged —
and merged items are CLOSED, while GitLab's `listWork` hardcoded `state=opened` (its own
`issuesUrl` has taken `'opened' | 'closed' | 'all'` all along). GitHub had the same shape with
`state: 'open'`. So every finished item was INVISIBLE to any caller that asked for it: a mirror
rebuilding itself, an audit, a report. The pilot never noticed because it only ever asks for open
work, which is exactly why the bug could live this long.

- the host's open/closed axis now follows the REQUEST: `merged` in `query.states` asks the host for
  `all` (GitLab) / `state=all` (GitHub), and the local state filter still decides what the caller sees
- the shared contract suite asserts the rule as a rule: after driving an item to `merged`, the board
  must list it when `merged` is asked for. A state that can be set but not listed is invisible.
- verified against the REAL GitLab project: `resync` went from `projected 1` to `projected 7, failed 0`,
  and a raw REST read of the Notion database now holds all seven rows with the right Status
  (six `merged`, one `blocked`), with no duplicates across two resync runs

HONEST LIMIT: the board suites' doubles answer list queries without honouring the host's open/closed
axis, so the new contract assertion passed against GitLab and GitHub BEFORE the fix — the live run is
what caught it. The doubles are kinder than the hosts again (the ledger's #13 class), and making them
model the axis is the next thing this rule needs in order to be defended by tests.

Core 244/241/0, boards 147/147/0, 0 TS errors.
Fix is 5e9b1fd. Found the first time a MIRROR asked a board for finished work — the pilot never asked, so
the defect was invisible for as long as the only caller wanted open items.
A page has no body FIELD, so the adapter documented the loss and dropped
`spec.body`. That was honest, and still wrong for the first real user of the
projection: eight pages, eight titles, eight statuses, and not one word about
what the work was.

- `createWork` writes the body as paragraph blocks: one block per LINE, runs of
  at most Notion's 2000-character ceiling, at most 100 blocks per request (the
  first batch rides the create, the rest are appended). Nothing is truncated and
  nothing is reflowed: `blocksToText(bodyBlocks(b)) === b` is an invariant with
  its own test, blank lines and a 2500-character line included.
- reads read it back (`readContent`, default true): one request per page, plus
  one per 100 blocks, which `listWork` pays once per row. An item whose text
  silently reads as empty is how an agent gets sent to work on a description
  nobody gave it, so the opt-out is documented as the trade it is.
- an idempotent re-create REPAIRS: content that is a strict PREFIX of the spec's
  text is completed by APPENDING the missing lines, and a page whose content is
  anything else is returned untouched. Append is the only operation this adapter
  ever uses on content it did not write.
- a content write that fails is classified and says what it is: the page was
  filed, its content is incomplete, and re-running the same key appends the
  missing lines. And a 400 is no longer reported as a missing column option —
  Notion answers 400 for refused CONTENT as well.
- `plainText` now reads `text.content` as well as `plain_text`: it returned ''
  for the runs this adapter had just written, which the round-trip test caught
  on its first run.

`canTextSearch` still means TITLES ONLY: reading a body back is not searching
it, and the tests assert both halves.
…c completes a copy

The projection sent `labels: []` (hardcoded, so Notion's Labels column could
never fill) and trusted its id map — which knows THAT a copy exists and never
whether the copy is complete.

- the authority's labels ride the create. A mirror board that cannot record them
  refuses it (fail-closed, which is what the Notion adapter does), and the
  operator opts out once with `labels: false` on that mirror; nothing here
  guesses which failure was about labels.
- `resync` RE-ASSERTS the create for every item instead of trusting the map, so
  a copy filed before text and labels were carried is completed by the rebuild
  that was going to happen anyway. `mirror.written` says "created the mirror
  item" only when one was actually created.
- `mirrorsList()` and the pilot's per-tick line report `labels`, so a projection
  filed without them is visible configuration rather than an always-empty column.
- the core double now records `spec.labels` and completes a copy on an
  idempotent hit, like the real adapters do: a double that ignores a field
  cannot tell you the field is being dropped.
…efused

`labelsProperty: Labels` sat in takumi.yaml, reached the provider factory, and
vanished: the factory forwarded a hand-written subset (two or three options per
adapter). A live projection filed eight pages with an empty Labels column and no
error anywhere. The same class swallowed `trustedAuthors`, `statusMap`,
`issueType`, `readContent`, `titleProperty` — every option an adapter documents
and the wiring never passed.

- one typed mapper per provider forwards every documented option, parsed as what
  it is (string, comma list, boolean, number, JSON object) and with no coercion:
  `readContent: yes` is refused, not read as false.
- `assertKnownProviderOptions` refuses a key the adapter does not read and names
  the ones it does; a credential in configuration is refused as env-only, and a
  transport or a clock as code-only. A typo like `database:` for `databaseId:`
  is now a startup error instead of a silent no-op.
- `--option key=value` (repeatable) reaches any documented option without this
  parser having to know every provider by heart.
- a stray Chinese character in an English comment is gone: the repository is
  English-only.
…is closed

- ADR-023 records the decision, its costs and its limits: content in batches of
  100 blocks and runs of 2000 characters, the read-back that keeps `body`
  truthful, append-only repair, and the rule that a capability difference is a
  decision to make rather than a footnote to write.
- ADR-017: rule 5 now says what is mirrored (state, title, text, labels,
  comments) and `labels: false` is the configuration for a board that cannot
  hold them; the live verification and the re-asserting resync are recorded.
- ADR-022: the sentence claiming ADR-017's wiring was still open is corrected —
  it is closed, and the live-host report surface is the gap that remains.
- README: the claim that no adapter had ever run against a live board or host
  was false (GitLab and the Notion mirror both have), the Notion row no longer
  says it has no labels, and the new capability is in the status table.
Both were found on a LIVE run and neither was visible in the suite:

- #23: the projection carried a title and a status and nothing else — `labels: []`
  hardcoded, an id map trusted for completeness, and no way to hand Notion a body.
- #24: `labelsProperty: Labels` reached the provider factory and vanished, because
  the factory forwarded a hand-written SUBSET of every adapter's options. The same
  seam was swallowing `trustedAuthors`, `statusMap`, `issueType`, `readContent`.

The two class notes that came out of them are the durable half: a value that
parses and then disappears at a seam looks exactly like a feature that does not
work, and a placeholder filled with an empty literal can make a fail-closed guard
unreachable.
ADR-023 listed "content is not edited, only appended" among the things not done.
It is now a decision: rewriting a Notion page's content means deleting blocks this
adapter did not write (a person's note beside the description, a correction, a
checklist), because the host replaces content by removing and re-adding it.

What a rebuild guarantees is therefore COMPLETENESS — nothing the authority
carries is missing — and not currency of prose. The rejected alternative
(takumi-owned and human-owned sections) is recorded with its reason.
…ow-gates

feat: mirrors closed out, and the deterministic rules gate a workflow (ADR-023, ADR-024)
@sscodeai
sscodeai merged commit 5e533b4 into dev Sep 30, 2026
2 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