Skip to content

feat: a node that was never authored is never advised to freeze, and prose that claims output must prove it - #207

Merged
TinDang97 merged 4 commits into
mainfrom
feat/affordance-truth
Sep 1, 2026
Merged

feat: a node that was never authored is never advised to freeze, and prose that claims output must prove it#207
TinDang97 merged 4 commits into
mainfrom
feat/affordance-truth

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

The affordance-truth milestone, closed 7/7 and driven through ADD itself.

1 — A node that was never authored is never advised to freeze

direction.md states the design plainly — "There is no author verb — you fill those sections by editing that file directly" — and no affordance in the engine knew it. BEAT_NEXT["direction"] mapped the whole beat to add freeze {slug}, so from the moment add new Task wrote a file of placeholders, five surfaces recommended a verb freeze is structurally guaranteed to refuse.

The cost was never the wasted call. A scaffold and a finished contract read identically from status, so a node could sit unauthored for days inside a milestone that read as in progress — observed live in another repo, and true of this bundle's own v3-final-collateral since 2026-08-11.

The derived beat gains a scaffold rung between created and frozen, sorting first in todo under its own header:

$ add todo
1 open task(s):
scaffold:
  · fresh-thing → author fresh-thing's RULES, ASSUMPTIONS and CHECKS, then add freeze fresh-thing

It is computed from the same predicates the refusals callgives_unauthored, placeholders_in — never a copy, because two notions of "authored" is precisely how advice and refusal came to disagree. status, todo, new and the CARD scaffold all resolve through one map, and status's own stamp test, a third reading of the beat, is gone.

On Milestones the bad advice was not refused — it SUCCEEDED. placeholders_in reads RULES · ASSUMPTIONS · CHECKS and a Milestone body carries none of those three, so it returned [] for every milestone and ADD's one human approval was stampable against a node stating no goal, no scope and no exit criterion. Measured, not inferred:

$ add new Milestone probe-ms && add freeze probe-ms
freeze recorded at authority `process`      # with goal: <one line> untouched

_milestone_stubs closes it, deliberately narrow: goal · why · EXIT, the three the milestone lifecycle already reads, since milestone_done refuses on why: and on the EXIT tally. A guard reaching SCOPE and GROUND too would refuse real milestones whose ground is thin, and a guard everyone learns to widen past is worse than a narrow one that holds. It refuses this repo's own v3-final-collateral — the intended one-time authoring cost.

card_drift now compares the DERIVED beat, not the raw status: field. This repairs a falsified assumption recorded by a replan on 2026-08-17: freeze does not move status:, so a freshly frozen node advertised next: add freeze <slug> — the approval it had just passed — while todo and status derived build, and doctor reported it CLEAN. render_card goes through _next_verb for the same reason; reading the map raw put a third answer on the CARD, missing that a sealed-but-unbriefed task owes add brief first.

2 — A claim that the engine prints something is proven by running it

promised-capability-guard closed this class for the READMEs, and its own why: named the gap it left: those guards check nouns the engine exposes, never capabilities the prose promises. loop.md is where that bit. It told the reader add status shows goal not met (m/n exit criteria), and pointed at "the plan-vs-state line in add status". status prints neither. The first string did not even live in add.py any more — it had been reworded to milestone_goal_unmet inside milestone_done's refusal — so an existence anchor of the kind the README guard uses would have resolved it and passed.

Both sentences sat in the loop's Gather step, and the first was named as THE cue that starts the loop, so an agent following the skill waited for a signal the engine never sends.

The guard registers every skill-tree line naming a backticked add <verb> and a rendering, and proves each by driving that command against a bundle put into the state the claim describes. The anchor is captured stdout. An unregistered claim fails by name, quoting the sentence with its file and line. Both false claims are reworded to truth in all three live skill trees; the new Gather cue is add milestone-done refusing with milestone_goal_unmet (m/n exit criteria), and planned-but-unscaffolded tasks are found at the scaffold beat this PR ships. Both replacements are themselves registered and driven.

add.py is untouched by that task, asserted by a check that diffs it — building the missing status surface is a real improvement and a separate task. A guard that ships having made its own claims true has never once refused anything.

Verification

The gate refused both tasks' first PASS, and was right both times. Two distinct ID traps, worth knowing about:

  • a parametrized check binds nothing — pytest reports it as test_x[param] and the gate binds bare ids, so a green check sat there with its rule unbound;
  • a module name binds nothing either (test_tree_parity is a file, not a test).

Beyond those, five probe: clauses had no check at all. Eight checks added across the two tasks, contracts refrozen, then gated.

  • Red first: 12 of 21 checks failed against the pre-change engine for task 1. The green ones were deliberate counter-guards — a frozen node's affordances unchanged, the quick one-call lane still closing, the verb count, the status: vocabulary, frontier ordering.
  • Dogfooded on a scratch bundle and on this repo's own.
  • 818 passed / 7 skipped across both roots. Four engine twins synced, both MD5 pins re-aimed, py3.10 syntax floor checked with ast.feature_version.

Reviewer notes

  • A frozen Reject from another task shaped the design. build-orient froze R:T2SCAN — "building a report must not read a single body" — and status needed the body. That Reject is not this task's to weaken, so _is_scaffold is one function with two tiers: T0 always, T2 only when the caller already holds the body. Disclosed residual: a node with an authored gives: but still-template RULES reads authored to status alone. todo and freeze both catch it, and it is recorded in the docstring.
  • Two exit criteria were amended rather than ticked, each carrying its amendment note in the milestone body: criterion 1 said "every surface", which no surface bound by R:T2SCAN can satisfy; criterion 4 described the wider guard scope that was deliberately narrowed. Neither was ticked as written.
  • The milestone closes checked by Tin Dang (unattended) — the ticks came from a process, not a human at a terminal, and the attribution shipped in feat: ceremony proportional to kind and size, and add check for the tally the gate reads #206 records that rather than letting a claimed name stand alone.
  • test_new_scaffold.py's pinned affordance is re-aimed, not deleted; two incumbent value pins in test_todo.py and test_doctor.py likewise. The test_doctor fixture was replacing a string the body no longer carried — a silent no-op its own assertion caught.

Direction beat for `authoring-beat-named`. Nothing is frozen — the ONE
approval is the human's and was not stamped.

The node was authored before #206 merged, so it was repaired before being
trusted. Nine referents were stale: every `add.py:` line anchor had drifted
with that PR's +322 lines (freeze's refusal 1260->1394, placeholders_in
2270->2595, status 1777->2015, todo, new, BODIES, milestone_done, the quick
lane), and one check still pinned the verb count at 22 when `add check` had
made it 23.

Both headline claims re-measured against the post-merge engine rather than
taken from the node's prose. Both hold: `add new Milestone` then `add freeze`
returns `freeze recorded at authority process` with `goal: <one line>`
untouched, and `placeholders_in` returns [] for any Milestone body, because it
reads RULES · ASSUMPTIONS · CHECKS and a Milestone carries none of them. ADD's
one human approval is stampable today against a node stating no goal, no
scope and no exit criterion.

Two defects found in the node itself while driving it:

`sensitivity: high` is not a value the engine recognises. SENSITIVITY_FLOOR
maps mechanical|data|architecture|security and `.get(sens, "process")` turns
anything else into the LOWEST floor, so both affordance-truth tasks computed
`process` where they meant `plan` — every other node in this bundle says
`architecture`. Corrected here; the underlying refusal is filed as a lesson
against specs/method and is queued for the enforcement node.

The milestone-advice check asserted on `status`, whose `next:` line targets
the task frontier and never names a milestone at all — it passed with no fix
applied, which is exactly the vacuous shape R:VACUOUS_GUARD forbids.
Retargeted to `new`, the surface that actually emits the advice.

21 checks. 12 RED for the right reason; the 9 green are deliberate regression
guards — a frozen node's affordances unchanged, the quick one-call lane still
closing, the verb count, the `status:` vocabulary, frontier ordering, and the
empty-frontier affordance. test_new_scaffold.py's pinned assertion is RE-AIMED
rather than deleted: the scaffold's `next:` stays a pinned interface, it was
the value that was wrong.

One contract detail the PLAN implies without stating: `_beat_of` returning
`scaffold` needs a matching entry in `todo`'s sort map, or scaffolds sort
after verify instead of first. `active()` reads the `status:` field rather
than the beat, so no node drops off the frontier.

author: Tin Dang
`direction.md` states the design — "There is no author verb — you fill those
sections by editing that file directly" — and no affordance in the engine knew
it. `BEAT_NEXT["direction"]` mapped the whole beat to `add freeze {slug}`, so
from the moment `add new Task` wrote a file of placeholders, five surfaces
recommended a verb `freeze` is structurally guaranteed to refuse. The cost was
never the wasted call: a scaffold and a finished contract read identically from
`status`, so a node could sit unauthored for days inside a milestone that read
as in progress.

The derived beat gains a `scaffold` rung between created and frozen, computed
from the SAME predicates the refusals call — `gives_unauthored` at T0,
`placeholders_in` at T2 — never a copy of them. `status`, `todo`, `new` and the
CARD scaffold all resolve through one map, and `status`'s own stamp test (a
third reading of the beat) is gone.

On Milestones the bad advice was not refused: it SUCCEEDED. `placeholders_in`
reads RULES · ASSUMPTIONS · CHECKS and a Milestone body carries none of those
three, so it returned [] for EVERY milestone and ADD's one human approval was
stampable against a node stating no goal, no scope and no exit criterion.
`_milestone_stubs` closes that, deliberately narrow: goal · why · EXIT, the
three the milestone lifecycle already reads, since `milestone_done` refuses on
`why:` and on the EXIT tally. A guard reaching SCOPE and GROUND too would refuse
real milestones whose ground is thin, and a guard everyone learns to widen past
is worse than a narrow one that holds. This bundle's own `v3-final-collateral`
is refused by it — the intended one-time authoring cost.

`card_drift` now compares the DERIVED beat, not the raw `status:` field. The
2026-08-17 replan recorded A5 as falsified and this is its repair: `freeze` does
not move `status:`, so a freshly frozen node advertised `next: add freeze <slug>`
— the approval it had just passed — while `todo` and `status` derived `build`,
and `doctor` reported it CLEAN. `render_card` goes through `_next_verb` for the
same reason: reading BEAT_NEXT raw put a third answer on the CARD, missing that
a sealed-but-unbriefed task owes `add brief` first.

R:T2SCAN kept honest. `build-orient` froze "building a report must not read a
single body", and that Reject is not this task's to weaken — so `_is_scaffold`
is one function with two tiers: T0 always, T2 only when the caller already holds
the body. `status` gets the T0 answer, `todo` and `freeze` the complete one.
Residual, recorded rather than hidden: a node with an authored `gives:` but
still-template RULES reads authored to `status` alone.

Two incumbent value pins re-aimed, neither weakened: `test_todo_beat_is_stamp_derived`
now expects `scaffold` where it expected `direction` (the token moved, the
stamp-derivation it covers did not), and `test_sync_repairs_card_drift`'s fixture
drifts from `beat: scaffold` — it was replacing a string the body no longer
carried, a silent no-op its own assertion caught.

The gate refused the first PASS with six unbound rules and was right: A5 and A12
were proven but never cited, A4 and A11 had no check at all, and `test_tree_parity`
is a MODULE name that binds nothing. Three checks added, citations corrected,
contract refrozen.

26 checks. Receipt 3: 795 passed / 7 skipped, exit 0, kind test-ids, freshness
content. Gated PASS at authority `plan`, freshness fresh. Four engine twins
synced, ENGINE_MD5 re-aimed.

author: Tin Dang
…nning it

`promised-capability-guard` closed this class for the READMEs and its own `why:`
named the gap it left — those guards check nouns the engine EXPOSES, never
capabilities the prose PROMISES. `loop.md` is where the gap bit. It told the
reader `add status` shows `goal not met (m/n exit criteria)` and pointed at "the
plan-vs-state line in `add status`". `status` prints neither, and the first
string no longer exists anywhere in `add.py` — it was reworded to
`milestone_goal_unmet` inside `milestone_done`'s refusal — so the existence
anchor the README guard uses would have resolved it and passed. The claim sat in
the loop's Gather step, named as THE cue that starts the loop, so an agent
following the skill waited for a signal the engine never sends.

The guard registers every skill-tree line that names a backticked `add <verb>`
AND claims a rendering, and proves each one by DRIVING that command against a
bundle put into the state the claim describes. The anchor is captured stdout.
An unregistered claim fails by name, quoting the sentence with its file and
line, so a new claim cannot join the corpus silently.

Both false claims reworded to what the engine does today — in all three live
skill trees. The Gather cue is now `add milestone-done <slug>` refusing with
`milestone_goal_unmet (m/n exit criteria)`, and planned-but-unscaffolded tasks
are found at the `scaffold` beat in `add todo`, which the sibling task shipped
an hour ago. Both replacements are themselves registered and driven.

M4 held: `add.py` is untouched by this task, asserted by a check that diffs it.
Building the missing `status` surface is a real improvement and a separate task;
a guard that ships having made its own claims true has never once refused.

The guard caught a live consequence of the sibling task while being written. Its
first driver for "`add todo` counts them down" built a node with template RULES,
which now sits at the `scaffold` beat and is told to author rather than given the
countdown. That is better guidance, not a regression — the countdown is the
DIRECTION beat's hint and still renders there — so the driver was corrected to
build the claim's real state rather than the engine to match a bad driver.

The gate refused the first PASS with six unbound rules and was right twice over:
five probes had no check, and `test_loop_claims_are_repaired_in_all_three_trees`
was PARAMETRIZED, so it reported as `test_x[skills]` and bound nothing while
sitting green. De-parametrized, five checks added, contract refrozen.

21 checks. Receipt 2: 815 passed / 7 skipped, exit 0, kind test-ids, freshness
content. Gated PASS at authority `plan`, freshness fresh.

author: Tin Dang
Both tasks gated PASS at authority `plan`. Two criteria were AMENDED before
being ticked rather than ticked as written, each carrying its amendment note in
the milestone body:

Criterion 1 said every surface that derives a `next:` names authoring. No
surface bound by `build-orient`'s frozen R:T2SCAN can satisfy that — `status`
must not read a body — so it is reworded to every surface that CAN read the
body, naming the one escaping shape (authored `gives:`, template RULES) which
`todo` and `freeze` still catch and which `_is_scaffold`'s docstring records.

Criterion 4 said `freeze` refuses a Milestone whose CARD, SCOPE, GROUND or EXIT
are template. M7 was deliberately narrowed to goal · why · EXIT — the three the
milestone lifecycle actually reads — so the criterion is narrowed to match what
shipped rather than overstating it.

The close line reads `checked by Tin Dang (unattended)`: the ticks were made by
a process, not by a human at a terminal, and #206's attribution records that
distinction rather than letting a claimed name stand alone.

author: Tin Dang
import tempfile
from pathlib import Path

import pytest
@TinDang97
TinDang97 merged commit c678908 into main Sep 1, 2026
8 checks passed
TinDang97 added a commit that referenced this pull request Sep 1, 2026
…ing seal

Direction beat for `risk-accepted-integrity`: the frozen-contract draft and its
red checks. No engine change yet — the freeze is the human seam and this node
declares `sensitivity: security`, so the floor is `human`.

The finding, measured rather than inferred. All 16 integrity refusals in `gate`
are conditioned on `verdict == "PASS"`. Against the current engine, a Task
created seconds earlier — still carrying `goal: <one line>` and every template
section, never frozen and never briefed — reached `done` in three calls:

    add run <slug> -- true
    add gate <slug> RISK-ACCEPTED --reason "probing"
    add done <slug>

R:UNSEALED, the post-freeze drift check, R:UNBRIEFED, the placeholder check and
R:UNDECLARED_SENSITIVE all read PASS only, so the lane meant to be the
SCRUTINISED one carries no scrutiny at all. This is #206's own lesson one
verdict over: skipping `freeze` did not FAIL the post-freeze guards, it
SWITCHED THEM OFF. That hole was closed for PASS and left open next door.

Two smaller findings fell out of the same read, both re-characterized by
execution rather than taken from notes:

  * `_paths_touch` over-matches on string prefix, not path segment. The
    recorded symptom (`scope: s` defeating the A17 floor) is false — probed, it
    returns False. The real defect is the opposite and lives in the EXEMPTION
    direction of R:UNDECLARED_SENSITIVE: `scope: src` exempts a changed
    `srcfoo/secret.yaml`, and `secrets_public/x` matches `secrets/**`.
  * an unrecognised `sensitivity:` degrades to the LOWEST floor. `high` and
    `critical` both read `process`. This bit two real nodes on the
    affordance-truth branch, which declared `high` and silently floored to
    `process` instead of `plan`.

The contract splits the refusals in two: those protecting the RECORD (was it
frozen, did the contract drift, is the body still a template, did the build
touch an undeclared sensitive path) bind every verdict; those judging the
EVIDENCE (stale receipt, non-zero exit, unbound covers, coverage gap) stay
PASS-only, because signing for imperfect evidence is what the verdict is FOR.

19 checks: 13 red for the right reason, 6 green as counter-guards pinning
behavior that must NOT change — a lone refreeze still satisfies the seal, a
legitimate RISK-ACCEPTED on a frozen node still closes, a first-call gate still
hears the receipt message rather than a security refusal, and a HARD-STOP stays
recordable on an unfrozen node (refusing it would only lose the finding).

Suite: 824 passed / 13 failed / 7 skipped across both roots — the 13 are these.

Filed while dogfooding the `scaffold` beat from #207, which earned its keep: it
refused to advance this node while `gives:` still read as template, catching a
`<slug>` inside backticks that `status` alone would have shown as `direction`.

author: Tin Dang
TinDang97 added a commit that referenced this pull request Sep 1, 2026
… the ONE approval asks its questions out loud (#208)

* test(engine): RISK-ACCEPTED signs for weak evidence, never for a missing seal

Direction beat for `risk-accepted-integrity`: the frozen-contract draft and its
red checks. No engine change yet — the freeze is the human seam and this node
declares `sensitivity: security`, so the floor is `human`.

The finding, measured rather than inferred. All 16 integrity refusals in `gate`
are conditioned on `verdict == "PASS"`. Against the current engine, a Task
created seconds earlier — still carrying `goal: <one line>` and every template
section, never frozen and never briefed — reached `done` in three calls:

    add run <slug> -- true
    add gate <slug> RISK-ACCEPTED --reason "probing"
    add done <slug>

R:UNSEALED, the post-freeze drift check, R:UNBRIEFED, the placeholder check and
R:UNDECLARED_SENSITIVE all read PASS only, so the lane meant to be the
SCRUTINISED one carries no scrutiny at all. This is #206's own lesson one
verdict over: skipping `freeze` did not FAIL the post-freeze guards, it
SWITCHED THEM OFF. That hole was closed for PASS and left open next door.

Two smaller findings fell out of the same read, both re-characterized by
execution rather than taken from notes:

  * `_paths_touch` over-matches on string prefix, not path segment. The
    recorded symptom (`scope: s` defeating the A17 floor) is false — probed, it
    returns False. The real defect is the opposite and lives in the EXEMPTION
    direction of R:UNDECLARED_SENSITIVE: `scope: src` exempts a changed
    `srcfoo/secret.yaml`, and `secrets_public/x` matches `secrets/**`.
  * an unrecognised `sensitivity:` degrades to the LOWEST floor. `high` and
    `critical` both read `process`. This bit two real nodes on the
    affordance-truth branch, which declared `high` and silently floored to
    `process` instead of `plan`.

The contract splits the refusals in two: those protecting the RECORD (was it
frozen, did the contract drift, is the body still a template, did the build
touch an undeclared sensitive path) bind every verdict; those judging the
EVIDENCE (stale receipt, non-zero exit, unbound covers, coverage gap) stay
PASS-only, because signing for imperfect evidence is what the verdict is FOR.

19 checks: 13 red for the right reason, 6 green as counter-guards pinning
behavior that must NOT change — a lone refreeze still satisfies the seal, a
legitimate RISK-ACCEPTED on a frozen node still closes, a first-call gate still
hears the receipt message rather than a security refusal, and a HARD-STOP stays
recordable on an unfrozen node (refusing it would only lose the finding).

Suite: 824 passed / 13 failed / 7 skipped across both roots — the 13 are these.

Filed while dogfooding the `scaffold` beat from #207, which earned its keep: it
refused to advance this node while `gives:` still read as template, catching a
`<slug>` inside backticks that `status` alone would have shown as `direction`.

author: Tin Dang

* test(engine): the ONE approval asks its questions out loud

Direction beat for `freeze-interview`: the frozen-contract draft and its red
checks. No engine change yet — the freeze is the human seam.

`freeze` already refuses an incomplete contract eight ways: template
placeholders, an unauthored `gives:`, collapsed surfaces, unswept (dimension,
surface) pairs, an unbudgeted explore. Every one of those checks the DOCUMENT.
None checks the CONVERSATION.

`## ASSUMPTIONS` is by construction a list of silences the AI filled in on the
human's behalf, each line carrying the reading taken and the cost if it is
wrong. The sweep made the AI WRITE DOWN what it decided. Nothing made it ASK.
So ADD's ONE human approval is a single stamp that says nothing about whether
the human was ever shown a single decision they are approving.

At a human floor `freeze` now refuses until every open decision has been put to
a human and answered (R:UNINTERVIEWED). Two calls, both ratified by the human
this session:

  * ENFORCED, not advisory. This repo's own record is the argument: three probe
    campaigns showed recommended prose gets routed around, which is why `brief`
    became a checkpoint (R:UNBRIEFED) rather than advice.
  * ASSUMPTIONS + REJECTS. An assumption is a silence the AI filled and a Reject
    is a boundary the AI drew — both are decisions the human never made but will
    be held to. The Musts came FROM the human, so re-asking them is noise, and
    an `n/a` retirement already states its own reason.

Design consequence found while reading `freeze`: it computes
`authority = authority or authority_for(graph, cid)`, so an explicit
`--authority process` DOWNGRADES even a `security` node. Keying the interview on
the passed flag would therefore have shipped the guard with its own one-flag off
switch, so M5 keys it on the COMPUTED floor and E1/test pins that.

Answers persist as evidence, not a boolean: one `act: interview` stamp carrying
a digest of exactly what was interviewed, plus a `.d/interviews/<n>.md` sidecar
it references — the same sidecar shape `.d/runs/<n>.md` already uses, so no body
section is renumbered (R:RENUMBER; the engine keys sections by name and number).
Staleness follows `direction:` and `brief:`: edit an interviewed assumption and
the digest moves, so the interview re-opens. Edit a Must and it does not.

`correct` is a real verdict and does NOT complete an interview — it is cleared
by editing the item, which moves the digest and requires a fresh pass. `defer`
does complete one: a human may accept a risk knowingly.

21 checks: 18 red for the right reason (`add.interview` absent, `freeze` does
not refuse, the three skill trees do not teach it), 3 green as counter-guards
pinning behavior that must NOT change — a `process`-floor node is never
interviewed, the interview runs LAST so nobody is asked about template slots,
and the section census is unchanged.

author: Tin Dang

* fix(engine): RISK-ACCEPTED signs for weak evidence, never for a missing seal

Build beat for `risk-accepted-integrity`. All 19 checks green.

`gate`'s sixteen integrity refusals were each written `verdict == "PASS"`, so
RISK-ACCEPTED walked past every one of them and `done` accepted the resulting
stamp. Measured before the fix: a Task created seconds earlier — still every
template slot, never frozen, never briefed — reached `done` in three calls.

The sixteen inline conditions are now ONE predicate over two named tuples, so
the classification is reviewable in one place instead of being re-decided at
each site:

  * INTEGRITY_REFUSALS protect the RECORD — unsealed, drift, unbriefed,
    placeholders, undeclared_sensitive, phantom_scope, and the three explore
    equivalents. They bind every verdict that APPROVES or CLOSES.
  * EVIDENCE_REFUSALS judge the RUN — stale_receipt, failed_run,
    unbound_covers, hollow_explore, no_security_lens. PASS only: signing for
    imperfect evidence is what RISK-ACCEPTED is FOR, and three of them already
    named it as their own remedy.
  * HARD-STOP is refused by neither. It never closes a task, so refusing it
    would only stop a finding being written down.

`_binds` raises on an unclassified name, so a refusal added later cannot
silently inherit either tier. The verdict literal now appears exactly once in
`gate`, as `closes`.

`done` gained the seal check: any (re)freeze must precede the entitling gate. A
refreeze recorded afterwards (the re-cross pattern) is not required to.

`_paths_touch` matches whole path SEGMENTS. The dangerous direction was the
EXEMPTION clause of R:UNDECLARED_SENSITIVE, where a string-prefix read let
`scope: src` sign for a `srcfoo/` the node never declared, and
`secrets_public/` answer for `secrets/**`. An empty side now matches nothing.

`sensitivity:` floors UP when unreadable. `SENSITIVITY_FLOOR.get(v, "process")`
sent every unrecognised value to the LOWEST floor; `high` and `critical` both
read `process`, which is how two real nodes on the affordance-truth branch
floored at `process` where they meant `plan`. `new` now refuses the value
outright — deliberately breaking that function's "slots, never validation"
stance, because `sensitivity:` is not a prose slot but the enum that computes
the floor, and `init` already refuses an unknown `--profile` for the reason.

Four fixtures were corrected, none weakened; every assertion is unchanged:

  * two of my own — one anchored on `depth: standard`, which `new` does not
    write unless supplied, so it was a silent no-op and passed against nothing;
    one used `brief()` (which COMPILES) instead of `brief_stamp()` (which
    records), and one built its bundle where `gate`'s `root.parent` could not
    see the git repo.
  * two incumbent — `test_done_transitions_with_gate` and `_make_done` in
    test_reopen.py both forged a gate stamp with no freeze, which is exactly
    the record M6 now refuses. They assert that an entitling gate transitions;
    that stays true, so they gained the seal the method always implied.

Four engine twins synced; ENGINE_MD5 re-aimed 1bf61710 -> 4c2a048d.

author: Tin Dang

* fix(engine): `unbriefed` is an EVIDENCE refusal, not an integrity one

Correcting the tier split shipped one commit ago. I classed R:UNBRIEFED as
INTEGRITY; the full suite disagreed, and it was right.

`test_brief_gate.py::test_non_pass_verdicts_are_never_blocked` carries
`covers: M3` of the brief-gate task's FROZEN contract — "a verdict is how a node
LEAVES a bad state (same stance as R:TRAP)". That Reject belongs to another task
and is not this one's to weaken, so the refusal moves rather than the test.

It also reads correctly on the merits. A missing brief says the BUILD ran
without the compiled prompt — a fact about how the run was driven, which is what
the evidence tier is for. The record itself still cannot be forged under any
verdict: the seal (R:UNSEALED), the post-freeze drift check and the placeholder
guard all stay INTEGRITY, so a RISK-ACCEPTED still cannot close an unfrozen,
drifted or template node.

Same shape as the R:T2SCAN conflict resolved during affordance-truth: when a
frozen Reject from a neighbouring task contradicts a design choice here, the
frozen one wins and the reasoning is recorded at the site.

Twins resynced; ENGINE_MD5 re-aimed.

author: Tin Dang

* fix(engine): close risk-accepted-integrity — the gate refused its own first PASS

The gate refused the first PASS on three unbound referents, and was right on all
three:

  * A13 and A14 were the two assumptions added to close the sweep during
    authoring. Their checks were WRITTEN (the HARD-STOP counter-guard and the
    backslash case) but never declared in `## CHECKS`, so the gate bound nothing
    — docstring `covers:` is not the binding, the node's CHECKS section is.
  * `test_no_existing_refusal_was_narrowed` was DECLARED covering R:WIDEN and
    never written at all.

Writing it found the defect it was for. Rewriting sixteen inline conditions into
two named tuples is precisely the edit where a refusal quietly disappears, and
one had: R:UNFROZEN_EXPLORE was listed in INTEGRITY_REFUSALS but never dispatched
through `_binds`, because that refusal is UNCONDITIONAL — it refuses HARD-STOP
too. Routing it through `_binds` would have narrowed an existing refusal to buy
tidiness, so it comes out of the tuple with the reason recorded at the site.

The check's own arithmetic was wrong too (14 names; there are 13). Replaced with
an explicit member set rather than a smaller count — naming them pins strictly
more than counting them, and a dropped refusal now fails by name.

Refroze, re-briefed, re-ran, gated PASS at `human` on receipt 2: 20/20 checks
bound, freshness fresh.

author: Tin Dang

* feat(engine): the ONE approval asks its questions out loud

Build beat for `freeze-interview`. All 21 checks green; 858 passing across both
roots.

`add interview <slug>` compiles every non-`n/a` assumption and every Reject into
a numbered question carrying the reading taken and the cost if it is wrong. Bare
it is a read. With `--answer <id>=confirm|correct|defer --by "<name>"` it writes
a `.d/interviews/<n>.md` sidecar and appends one `act: interview` stamp
referencing it. At a human floor `freeze` refuses until every decision is
answered -> R:UNINTERVIEWED.

It is the LAST refusal in freeze's ladder: everything above says the contract is
unfinished, and there is no sense putting template text to a human. It is also
the only one that checks the CONVERSATION rather than the document.

Keyed on `authority_for`, never on the `--authority` argument, since the line
below it is `authority or authority_for(...)` — reading the flag would have let
`--authority process` switch the interview off on a security node.

Staleness follows `direction:` and `brief:`. `interview_gap` reads the stamp
whose digest MATCHES the current text, not the latest: revert an edit and the
earlier interview is the one that answers for this text. `correct` never
completes an interview — it is cleared by editing the item, which moves the
digest. `defer` does: a human may accept a risk knowingly.

Adding the 24th verb rippled exactly as this repo's record says it does — 15
failures across six registries and five budget pins, every one a real staleness:

  * the CLI verb-set registry, the book's command reference (in `docs/`, not the
    root copy I edited first), and both READMEs' "23-verb kernel" line.
  * `test_no_new_verb_in_the_cli_surface` re-aimed 23 -> 24. Its claim is that
    the AUTHORING-BEAT task added no verb; a later task legitimately adding one
    moves the value without weakening that.
  * SKILL.md went 176 -> 179. One of the five budget guards is literally
    R:BUDGET_BUMP, so the addition was funded by COMPRESSING the freeze bullet
    it sits beside, back to 176 exactly.
  * `_open_decisions` returned a `"kind"` key, which collided with the receipt
    KIND vocabulary a separate guard enumerates against the skill corpus.
    Renamed to `"of"` rather than teaching the skill a word it does not mean.

Two security fixtures (`test_gate_coverage`, `test_sensitive_path_floor`) called
`add.freeze` and discarded the result, so the new refusal made them fail on
R:UNSEALED one step later. They now interview first AND assert the freeze landed
— a fixture that silently skips the one approval tests no real path.

Dogfooded on this repo's own bundle: `add interview freeze-interview` puts all
12 of its own open decisions.

author: Tin Dang

* feat(engine): close freeze-interview — the gate refused its own first PASS

Same pattern as the sibling node, and the gate was right again: A1, A3 and
R:SELFANSWER were unbound.

  * A3's staleness was proven by a check that cited only M7 and E3.
  * A1 and R:SELFANSWER had no check at all — and writing them found that my own
    SKILL.md compression had left the R:SELFANSWER instruction only in
    direction.md, WRAPPED ACROSS A LINE BREAK. This repo has silently unpinned a
    phrase that way before, so the sentence is reflowed onto one line and the
    check asserts it line by line rather than against the whole file.

`test_selfanswer_is_carried_by_prose_not_by_an_engine_claim` states the limit
plainly: `--by` is recorded verbatim, exactly as `freeze --by` is, so nothing
here VERIFIES a human. Claiming otherwise would be the worse failure — a guard
that asserts what it cannot prove is how a stamp becomes theatre. The discipline
is prose, and the check proves the prose ships.

Gated PASS at `plan` on receipt 2: 23/23 bound, freshness fresh.

Worth recording: this node's own floor is `architecture` -> `plan`, so by M5 it
was EXEMPT from the refusal it ships. The feature did not gate itself, and that
is the design — a `plan` freeze has no human in the room to interview — but it
means the first real exercise of R:UNINTERVIEWED is the next security-floored
node, not this one.

Three lessons filed to specs/method.

author: Tin Dang

* fix(engine): an interview accumulates across passes over the same text

Found in the pre-merge review of #208, not by the suite. `freeze-interview` was
reopened to `build` rather than patched after close.

`interview_gap` returned on the FIRST stamp whose digest matched the node. Two
consequences, both wrong and in opposite directions:

  * a human who answered three decisions, was interrupted, and came back to
    answer the rest was still refused — with no way forward but editing the node
    to move the digest, which is the one thing that DISCARDS the answers;
  * had it read the last match instead, a follow-up `correct` would have been
    outranked by the earlier `confirm`.

Every pass over the same text is now folded in order, later winning. An
interview is a conversation, and two sittings about the same decisions are one
interview.

Fixing that exposed the second half. Each stamp recorded the FULL decision list
with `unanswered` for ids that pass had not touched, so folding made every pass
clobber the one before it. The stamp's answer map is now SPARSE — only what that
pass actually answered. The sidecar still lists every decision, because that is
the human-readable record; the stamp is the machine-readable delta.

Two checks, red first, and the second is the counter-guard for the first:
accumulate must not mean a stale `confirm` outranks a later `correct`.

862 passed across both roots. Twins synced, ENGINE_MD5 re-aimed.

author: Tin Dang

* chore(node): re-gate freeze-interview after the review fix

The reopen reset the gate; receipt 3 re-earns it at 25/25 bound.

author: Tin Dang
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