HOMELAB-1690: feat(friction): the L7 mechanism — ld.friction/v1, ld.proposal/v1 state machine, shadow bindings, R-28 triage sink #592

Merged
aaron merged 4 commits from plane/HOMELAB-1690-l7 into epic/LD4 2026-10-03 04:08:10 +00:00
Owner

HOMELAB-1690 · LD4.6 · contract §6 (L7). Brief: .rpiv/artifacts/epic-ld4/design-LD4.6-l7.md; orchestrator adjudication on the ticket supersedes (R-28 for the triage routine; the friction schema carries the tool name + error class and never arguments — schema law; 4.6 owns the shadow-binding mechanism; the kill arithmetic counts applied corrections; retention = bounded spool + stream bounds).

Companion PR: Homelab/pi-extensions#61 (pi-friction, the capture surface).

What

A new lab-director/dispatcher/friction/ package, the durable half of L7, plus the R-28 registry entry:

File What it is
line.go ld.friction/v1 — the append-only line, closed class/surface/severity sets, derivable fingerprint, subject fleet.ledger.friction.<entrykind>. The NO-ARGUMENTS LAW is enforced here too (forbidden key families at any depth; a where.ref that renders an invocation is refused).
proposal.go ld.proposal/v1 — one record per transition, the five states as pure functions (LegalEdge/Validate/DeriveState/Append), plus the L7-K0/K1/K2 kill arithmetic.
shadow.go the shadow-binding mechanism: (role, class) → variantPin + sample, SampleUnit (hash of the dispatch key, so the pin is re-derivable), Apply = sample 1.0, Revert/RevertAndProve = restore the recorded sha256 pin, proven against the bytes that pin names.
triage.go the R-28 sink over the existing director.PlaneSink seam: group by fingerprint → dedup before any write → one open ticket per fingerprint, within a ticket budget → one receipt row on fleet.ledger.friction.triage.
routines/registry.yaml R-28 friction-triage, registered paused (SHARED file, one entry only).

Verify lines mechanized

  1. A friction line becomes a ticket — TestAFrictionLineBecomesATicket: the fixture's three auto-captured tool-error lines (three sessions) + one blocking /friction line → exactly two create_item calls, bodies citing the friction id, fingerprint, session and trace refs; the aggregating class files nothing. TestASecondOccurrenceFilesNothing: a re-run with the open fingerprints files zero items and folds into comments.
  2. A proposal ships with a recorded verdict; the revert is pulled — TestProposalStateMachine drives the fixture: every legal edge derives its state, proposed→accepted→applied asserted with evidenceRefs naming the merged PR, and TestApplyAndRevertRestoreTheRecordedPinByteForByte proves the revert restores the recorded pin byte-for-byte (including a refusal case where the bytes moved).
  3. No-arguments law — the capture-surface table test plus the Go decoder's own refusals.
  4. Kill-criterion inputs are computable from the ledger rows — KillInputsFromLedger + EvaluateKill: applied==0 && observerCost>0 kills under L7-K2 whatever the ratio and with unitValue absent (the LD4.4 §7.1 R6 fallback); L7-K1 is explicitly undecided rather than a pass when the denominator does not exist; a single window never decides; E4Decay counts active routines only.

Anti-spam (a receipt requirement, not a hope)

Only severity: blocks or a fingerprint seen ≥3× files; at most DefaultTicketBudget (3) new tickets per run; work-not-done is deferred and printed. 50 annoyances across 5 surfaces → ≤3 tickets (TestTheTicketBudgetHolds).

Cross-implementation check

One ld.friction/v1 record is pinned byte-for-byte in this repo's test and in the capture surface's test. It caught a real divergence: Go omitted actor.sandboxEnv for a human actor while pi-friction stamped unbound. Both sides now stamp unbound for any actor with no sandbox.

Verification

cd lab-director/dispatcher
go vet ./...          # clean
go test -race ./...   # all packages ok (friction: 31 tests over fixtures; no sockets)

Deviations from the brief (both deliberate, both named in the entry)

  1. ticketBudgetPerRun is not a registry field. The registry loader is KnownFields(true) and has no such key; adding one is a routines/ schema change this ticket does not own. The bound lives in friction.DefaultTicketBudget and is named in the R-28 description.
  2. effects.writes is not a field (the Effects schema has reads only), so the entry declares reads: [jetstream:friction, jetstream:proposal, plane] and the entry's comment says Plane is the write.
  3. R-28 is paused for dep:routine-executor (the task brief's instruction, matching R-22/R-25): the mechanism ships here, the executor that schedules it does not — so no scriptRef pin is claimed.
  4. testdata/proposal-transitions.json rather than .yaml: the ledger row is JSON, so the fixture exercises the wire decoder (strict fields, closed schema) instead of a second serialization.

Not in this PR (named, not forgotten)

  • teardown/ spool drain and roles.json include: ["friction"] — the deployment half; the mechanism deliberately does not depend on them (spool-first).
  • P-L7-001…010 do not land in routines/policy.md: that is routines/ internals and belongs with the R-28 activation PR; duplicating the policy book would give the fleet two places for one rule.
  • prod manifests: R-28 rides the existing routine mechanism; the NATS subject filter for the two new subject families is an additive manifest change when the executor lands (and 4.10 consumes this ledger).

For 4.10 to consume

The proposal ledger (schema + append path + derived-state read), the friction ledger (schema + subjects + retention), the frictionRefs join, the shadow-binding mechanism (pin selection + a sample context-bindings.json + the provable revert), and R-28's ledger for its sampled scan.

HOMELAB-1690 · LD4.6 · contract §6 (L7). Brief: `.rpiv/artifacts/epic-ld4/design-LD4.6-l7.md`; orchestrator adjudication on the ticket supersedes (R-28 for the triage routine; the friction schema carries the tool name + error class and never arguments — schema law; 4.6 owns the shadow-binding mechanism; the kill arithmetic counts **applied** corrections; retention = bounded spool + stream bounds). Companion PR: Homelab/pi-extensions#61 (`pi-friction`, the capture surface). ## What A new `lab-director/dispatcher/friction/` package, the durable half of L7, plus the R-28 registry entry: | File | What it is | |---|---| | `line.go` | `ld.friction/v1` — the append-only line, closed class/surface/severity sets, derivable fingerprint, subject `fleet.ledger.friction.<entrykind>`. The **NO-ARGUMENTS LAW** is enforced here too (forbidden key families at any depth; a `where.ref` that renders an invocation is refused). | | `proposal.go` | `ld.proposal/v1` — one record per **transition**, the five states as pure functions (`LegalEdge`/`Validate`/`DeriveState`/`Append`), plus the L7-K0/K1/K2 kill arithmetic. | | `shadow.go` | the **shadow-binding mechanism**: `(role, class) → variantPin + sample`, `SampleUnit` (hash of the dispatch key, so the pin is re-derivable), `Apply` = sample 1.0, `Revert`/`RevertAndProve` = restore the recorded sha256 pin, proven against the bytes that pin names. | | `triage.go` | the R-28 sink over the **existing** `director.PlaneSink` seam: group by fingerprint → dedup before any write → one open ticket per fingerprint, within a ticket budget → one receipt row on `fleet.ledger.friction.triage`. | | `routines/registry.yaml` | **R-28 `friction-triage`**, registered `paused` (SHARED file, one entry only). | ## Verify lines mechanized 1. **A friction line becomes a ticket** — `TestAFrictionLineBecomesATicket`: the fixture's three auto-captured tool-error lines (three sessions) + one blocking `/friction` line → **exactly two** `create_item` calls, bodies citing the friction id, fingerprint, session and trace refs; the aggregating class files nothing. `TestASecondOccurrenceFilesNothing`: a re-run with the open fingerprints files **zero** items and folds into comments. 2. **A proposal ships with a recorded verdict; the revert is pulled** — `TestProposalStateMachine` drives the fixture: every legal edge derives its state, `proposed→accepted→applied` asserted with `evidenceRefs` naming the merged PR, and `TestApplyAndRevertRestoreTheRecordedPinByteForByte` proves the revert restores the recorded pin **byte-for-byte** (including a refusal case where the bytes moved). 3. **No-arguments law** — the capture-surface table test plus the Go decoder's own refusals. 4. **Kill-criterion inputs are computable from the ledger rows** — `KillInputsFromLedger` + `EvaluateKill`: `applied==0 && observerCost>0` kills under **L7-K2 whatever the ratio and with `unitValue` absent** (the LD4.4 §7.1 R6 fallback); L7-K1 is explicitly *undecided* rather than a pass when the denominator does not exist; a single window never decides; `E4Decay` counts **active** routines only. ## Anti-spam (a receipt requirement, not a hope) Only `severity: blocks` or a fingerprint seen ≥3× files; at most `DefaultTicketBudget` (3) new tickets per run; work-not-done is **deferred and printed**. 50 annoyances across 5 surfaces → ≤3 tickets (`TestTheTicketBudgetHolds`). ## Cross-implementation check One `ld.friction/v1` record is pinned byte-for-byte in this repo's test and in the capture surface's test. It caught a real divergence: Go omitted `actor.sandboxEnv` for a human actor while `pi-friction` stamped `unbound`. Both sides now stamp `unbound` for any actor with no sandbox. ## Verification ``` cd lab-director/dispatcher go vet ./... # clean go test -race ./... # all packages ok (friction: 31 tests over fixtures; no sockets) ``` ## Deviations from the brief (both deliberate, both named in the entry) 1. **`ticketBudgetPerRun` is not a registry field.** The registry loader is `KnownFields(true)` and has no such key; adding one is a `routines/` schema change this ticket does not own. The bound lives in `friction.DefaultTicketBudget` and is named in the R-28 description. 2. **`effects.writes` is not a field** (the `Effects` schema has `reads` only), so the entry declares `reads: [jetstream:friction, jetstream:proposal, plane]` and the entry's comment says Plane is the write. 3. **R-28 is `paused` for `dep:routine-executor`** (the task brief's instruction, matching R-22/R-25): the mechanism ships here, the executor that schedules it does not — so no `scriptRef` pin is claimed. 4. **`testdata/proposal-transitions.json`** rather than `.yaml`: the ledger row is JSON, so the fixture exercises the wire decoder (strict fields, closed schema) instead of a second serialization. ## Not in this PR (named, not forgotten) - `teardown/` spool drain and `roles.json` `include: ["friction"]` — the deployment half; the mechanism deliberately does not depend on them (spool-first). - `P-L7-001…010` do **not** land in `routines/policy.md`: that is `routines/` internals and belongs with the R-28 activation PR; duplicating the policy book would give the fleet two places for one rule. - `prod` manifests: R-28 rides the existing routine mechanism; the NATS subject filter for the two new subject families is an additive manifest change when the executor lands (and 4.10 consumes this ledger). ## For 4.10 to consume The proposal ledger (schema + append path + derived-state read), the friction ledger (schema + subjects + retention), the `frictionRefs` join, the shadow-binding mechanism (pin selection + a sample `context-bindings.json` + the provable revert), and R-28's ledger for its sampled scan.
The L7 self-improvement mechanism's payload and mechanism (contract §6):

- friction.Line — ld.friction/v1, closed class/surface/severity sets, derivable
  fingerprint, subject fleet.ledger.friction.<entrykind>. The NO-ARGUMENTS LAW
  is enforced in code: forbidden key families are refused at any depth and a
  where.ref that renders an invocation is refused (ReasonArgumentsPresent).
- friction.Transition — ld.proposal/v1, one record per TRANSITION, the five
  states as pure functions (Legal/Validate/DeriveState/Append). Append refuses
  self-acceptance, resurrection of a terminal pid, acceptance of a
  contract-class proposal by a non-operator, and applied without a merged PR.
- Kill arithmetic (L7-K0/K1/K2) as pure functions over ledger rows, with the
  L7-K2 fallback when unitValue is absent (LD4.4 §7.1 R6).
- friction.Bindings — the shadow-binding mechanism: pin selection sampled by a
  hash of the dispatch key, apply = sample 1.0, revert = restore the recorded
  sha256 pin, proven against the bytes that pin names (RevertAndProve).
- friction.Triage — the R-28 friction->Plane sink over the existing
  director.PlaneSink seam: group by fingerprint, dedup before any write, one
  open ticket per fingerprint, ticket budget, and a receipt row on
  fleet.ledger.friction.triage (log-only publisher default + JetStream stub).

Fixture-first: 30 tests over testdata/friction-lines.jsonl,
testdata/proposal-transitions.json and testdata/context-bindings.json; no
sockets. go vet + go test green.
R-28 is the L7 leg contract §6 names: it drains the friction ledger into deduped
Plane items (one per fingerprint; blocks or >=3 occurrences only; within a
per-run ticket budget) and nags the proposal ledger (proposals stuck
proposed/accepted past the SLA, shadows past their window with no verdict).

Registered PAUSED for dep:routine-executor, the same dependency R-22 and R-25
name: the mechanism ships in this ticket (dispatcher/friction/) but the executor
that schedules it does not, so the entry carries no scriptRef pin — an ACTIVE
script routine must pin its mechanism, a paused one must not pretend to.

Two deviations from the brief's proposed entry, both deliberate:
- no ticketBudgetPerRun field: the registry schema has no such key (loader is
  KnownFields(true)) and this ticket does not own the registry schema, so the
  bound lives in friction.DefaultTicketBudget and is named in the entry;
- no effects.writes: the Effects schema has no writes key; reads name
  jetstream:friction, jetstream:proposal and plane.

The registry header's RESERVED note records R-28's landing. No count assertion
is hard-coded anywhere, so nothing had to be made derived: TestRegistryMatchesCorpus
treats a post-R-23 id as visible growth when it names its owning ticket.

go test ./routines/ (parity, pins, corpus) and ./friction/ green.
HOMELAB-1690: test(friction): cross-implementation wire fixture + unbound sandbox for every actor
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 24s
CI Review / dispatcher-test (pull_request) Successful in 5m22s
6caa69e0ff
The Go decoder now decodes, byte-for-byte, the exact ld.friction/v1 record the
capture surface's test asserts (pi-extensions/pi-friction/tests/types.test.ts,
"the wire shape (cross-implementation)"). The fixture is pinned in both repos on
purpose: the capture surface and the triage sink are independent implementations,
and this is what keeps one schema from becoming two.

It caught a divergence on the first run: Go omitted actor.sandboxEnv for a human
actor while the capture surface stamps `unbound`. Stamp() now fills `unbound` for
ANY actor with no sandbox and Validate requires it present, so a reader never has
to distinguish "no sandbox" from "the writer left it blank".

go vet + go test ./friction/ green.
aaron force-pushed plane/HOMELAB-1690-l7 from 6caa69e0ff
All checks were successful
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 24s
CI Review / dispatcher-test (pull_request) Successful in 5m22s
to 71631d8b18
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 21s
CI Review / dispatcher-test (pull_request) Successful in 3m39s
2026-10-03 04:02:28 +00:00
Compare
Author
Owner

Re-id + rebase (HOMELAB-1690, fix worker)

Rebased onto epic/LD4 @ 2607c6e (R-29 / PR #594 landed). One conflict, in routines/registry.yaml's RESERVED IDS header — resolved to the merged totals (R-28 stays LD4.5b's on-call window, R-29 phase census, LD4.6 added) and then re-id'ed.

Re-id: L7 friction-triage R-28 → R-30 everywhere in this branch (registry entry id + header mention, and the 12 dispatcher/friction/** files that named it: docs, comments, the Plane-item description string in triage.go, test descriptions, testdata). R-28's reservation prose for 4.5b is untouched. No R-28 reference to LD4.6 remains in the tree.

Checks on head 71631d8 (lab-director/dispatcher):

  • go test -race ./routines/... ./cmd/routines/... → ok (routines 10.8s, cmd/routines 1.9s); go test -race ./friction/... → ok
  • go vet ./routines/... ./cmd/routines/... → clean
  • go run ./cmd/routines -check -registry ./routines/registry.yaml → 26 routine(s), 5 active, 21 paused, 0 retired (22 corpus + R-24 + R-25 + R-29 + R-30; R-28 is not yet on epic/LD4, so 27 once #596 / 4.5b lands)

Not merged — landing worker holds.

**Re-id + rebase (HOMELAB-1690, fix worker)** Rebased onto `epic/LD4` @ 2607c6e (R-29 / PR #594 landed). One conflict, in `routines/registry.yaml`'s RESERVED IDS header — resolved to the merged totals (R-28 stays LD4.5b's on-call window, R-29 phase census, LD4.6 added) and then re-id'ed. Re-id: L7 `friction-triage` **R-28 → R-30** everywhere in this branch (registry entry id + header mention, and the 12 `dispatcher/friction/**` files that named it: docs, comments, the Plane-item description string in `triage.go`, test descriptions, testdata). R-28's reservation prose for 4.5b is untouched. No R-28 reference to LD4.6 remains in the tree. Checks on head 71631d8 (`lab-director/dispatcher`): - `go test -race ./routines/... ./cmd/routines/...` → ok (routines 10.8s, cmd/routines 1.9s); `go test -race ./friction/...` → ok - `go vet ./routines/... ./cmd/routines/...` → clean - `go run ./cmd/routines -check -registry ./routines/registry.yaml` → `26 routine(s), 5 active, 21 paused, 0 retired` (22 corpus + R-24 + R-25 + R-29 + R-30; R-28 is not yet on epic/LD4, so 27 once #596 / 4.5b lands) Not merged — landing worker holds.
Author
Owner

Landing review — VERDICT: approve (no blocker findings). Reviewed head 71631d8 (post-rebase, post-re-id), fresh context, primary sources only: the diff, HOMELAB-1690's acceptance lines, the design brief .rpiv/artifacts/epic-ld4/design-LD4.6-l7.md, and the repo's own contracts (routines/registry.go, director/{handoff,plane}.go, sentinel/ledger.go, direction/ledger.go).

Evidence I re-derived myself (not read from the PR body)

  • CI on this head: run #1785 @ 71631d8 — all four jobs pass (dispatcher-test 3m39s, helm-validate, alert-receiver-test, receiver-test). Independently: go vet ./... clean and go test -race -count=1 ./... green in lab-director/dispatcher (24 packages ok, no data race, friction 1.1s). No sockets: friction/*.go imports no net/http/nats.
  • Registry: go run ./cmd/routines -check -registry ./routines/registry.yaml → 26 routines, 5 active, 21 paused, 0 retired. R-30 loads, status: paused, pausedReason: dep:routine-executor. Merged totals resolve cleanly now that the branch is rebased: 22 corpus + R-24 + R-25 + R-29 + R-30 = 26. The census test's floor is derived from the corpus fixture (len(corpusRows)+2), so R-30 required no test-count edit.
  • Re-id delta verified: registry.yaml diff vs origin/epic/LD4 is exactly the header amendment + the new entry. R-28's reservation prose for LD4.5b is byte-identical; no R-28 reference to LD4.6 survives in the tree (22 remaining hits are all 4.5b's reservation). R-30 is free on the epic.

Per-dimension findings

  1. Triage sink — pass. Fixture is 5 lines → 3 fingerprints → 2 material (3× tool-error ≥ MinOccurrences, 1× blocks); create_item count asserted = exactly 2, and the re-run over an open-fingerprint map asserts 0 new items + 2 fold comments. The 50-annoyances/5-surface case yields 3 filed + 2 deferred (printed, not dropped). Plan is a pure function over injected state, so the bounds are assertable without Plane.
  2. Proposal machine + ledger value type — pass. 14 fixture cases cover all 5 states, both legal paths, and every documented refusal (self-accept, applied-without-merged-PR, rejected-without-warrant, resurrection ×2, revert-without-revertRef, illegal edge, no-friction-ref, shadow-without-guard, pin-not-a-digest). DeriveState/Append are pure; the test replays through Append and asserts agreement with DeriveState. Shape matches sentinel/direction (<schema>/v1 const, closed EntryKind+Valid()+Subject(), payload-only, stamped in MarshalJSON, strict DisallowUnknownFields decode).
  3. Shadow binding — pass. SelectPin is a hash of ticket|role|class|attempt (re-derivable, 0.25±0.05 share over 2000 keys); the sample testdata/context-bindings.json loads validating. The revert is proven against the bytes the pin names, and refused (ReasonPinMismatch) when the bytes moved — the provable-revert line holds.
  4. Go-side no-arguments refusal — pass. 11 decoder cases (top-level + nested keys, rendered where.ref invocations, shell pipelines) all refused with ReasonArgumentsPresent, each citing schema:ld.friction/v1#no-arguments. Cross-implementation: I confirmed the pinned record + fingerprint 09e84eddcf81d8d7 is byte-identical in pi-extensions pi-friction/tests/types.test.ts:240, so the divergence-catching claim is real, not asserted.
  5. Kill arithmetic — pass. applied==0 && cost>0 kills under L7-K2 with unitValue nil (and is not rescued by a huge denominator); L7-K1 returns Decided=false — undecided, not a pass; one window never decides; E4Decay is active-only. Numerator counts applied (a reverted-but-applied record still counts), plus friction tickets that reached Done.
  6. Registry / id discipline — pass (see above).
  7. Territory — pass. Only lab-director/dispatcher/friction/** (new) + routines/registry.yaml (entry + header). No claims/routines internals beyond the entry; no policy-book edit claimed.

Non-blocking concerns (no fix required to merge; for the activation PR)

  • A closed shadow is not a loadable document. Bindings.Revert sets Sample = 0 + VerdictAt, but Bindings.Validate refuses sample <= 0 (asserted by TestBindingsRefusals/sample_zero). So the in-memory revert proof is sound, yet a reverted context-bindings.json would be refused by LoadBindings. Either allow sample == 0 when VerdictAt != nil, or document "delete the entry on revert" — worth pinning before the file is real.
  • Unnamed deviation from the brief: filter: q2_derivation is true in the entry, false in brief §3.2. true is the correct answer given kind: script (the loader's q2-true ⇒ code implication), but it is absent from the deviations list.
  • The read-before-write half is injected, not implemented: Triage.Run takes the open-fingerprint map from the caller; the reader that resolves "open ticket for this fingerprint" lands with the executor. Fine while paused; name it in the activation ticket.
  • Count nit: PR body says "friction: 31 tests"; the package has 30 top-level tests (87 RUN incl. subtests). No functional impact.
  • Cross-repo: Homelab/pi-extensions#61 still carries 14 R-28 references in pi-friction/** (README, index.ts, ledger.ts, types, tests) — that companion needs the same re-id to R-30.
  • Epic-side (not introduced here, worth a sweep): the registry header's "Active today (6)" enumerates R-28 (LD4.5b's on-call window) while no R-28 entry exists on epic/LD4 (census: 5 active). It becomes true when #596 lands.

Proceeding to merge on the landing lock (green CI on this head, verdict posted after it, no blocker findings).

**Landing review — VERDICT: approve (no blocker findings).** Reviewed head `71631d8` (post-rebase, post-re-id), fresh context, primary sources only: the diff, HOMELAB-1690's acceptance lines, the design brief `.rpiv/artifacts/epic-ld4/design-LD4.6-l7.md`, and the repo's own contracts (`routines/registry.go`, `director/{handoff,plane}.go`, `sentinel/ledger.go`, `direction/ledger.go`). ## Evidence I re-derived myself (not read from the PR body) - **CI on this head:** run **#1785 @ 71631d8** — all four jobs pass (dispatcher-test 3m39s, helm-validate, alert-receiver-test, receiver-test). Independently: `go vet ./...` clean and `go test -race -count=1 ./...` green in `lab-director/dispatcher` (24 packages ok, no data race, friction 1.1s). No sockets: `friction/*.go` imports no `net`/`http`/`nats`. - **Registry:** `go run ./cmd/routines -check -registry ./routines/registry.yaml` → **26 routines, 5 active, 21 paused, 0 retired**. R-30 loads, `status: paused`, `pausedReason: dep:routine-executor`. Merged totals resolve cleanly now that the branch is rebased: 22 corpus + R-24 + R-25 + R-29 + R-30 = 26. The census test's floor is derived from the corpus fixture (`len(corpusRows)+2`), so R-30 required no test-count edit. - **Re-id delta verified:** registry.yaml diff vs `origin/epic/LD4` is exactly the header amendment + the new entry. R-28's reservation prose for LD4.5b is byte-identical; no R-28 reference to LD4.6 survives in the tree (22 remaining hits are all 4.5b's reservation). R-30 is free on the epic. ## Per-dimension findings 1. **Triage sink — pass.** Fixture is 5 lines → 3 fingerprints → 2 material (3× `tool-error` ≥ MinOccurrences, 1× `blocks`); `create_item` count asserted = **exactly 2**, and the re-run over an open-fingerprint map asserts **0 new items + 2 fold comments**. The 50-annoyances/5-surface case yields 3 filed + 2 deferred (printed, not dropped). `Plan` is a pure function over injected state, so the bounds are assertable without Plane. 2. **Proposal machine + ledger value type — pass.** 14 fixture cases cover all 5 states, both legal paths, and every documented refusal (self-accept, applied-without-merged-PR, rejected-without-warrant, resurrection ×2, revert-without-revertRef, illegal edge, no-friction-ref, shadow-without-guard, pin-not-a-digest). `DeriveState`/`Append` are pure; the test replays through `Append` and asserts agreement with `DeriveState`. Shape matches `sentinel`/`direction` (`<schema>/v1` const, closed `EntryKind`+`Valid()`+`Subject()`, payload-only, stamped in `MarshalJSON`, strict `DisallowUnknownFields` decode). 3. **Shadow binding — pass.** `SelectPin` is a hash of `ticket|role|class|attempt` (re-derivable, 0.25±0.05 share over 2000 keys); the sample `testdata/context-bindings.json` loads validating. The revert is proven against the bytes the pin names, and **refused** (`ReasonPinMismatch`) when the bytes moved — the provable-revert line holds. 4. **Go-side no-arguments refusal — pass.** 11 decoder cases (top-level + nested keys, rendered `where.ref` invocations, shell pipelines) all refused with `ReasonArgumentsPresent`, each citing `schema:ld.friction/v1#no-arguments`. Cross-implementation: I confirmed the pinned record + fingerprint `09e84eddcf81d8d7` is byte-identical in `pi-extensions` `pi-friction/tests/types.test.ts:240`, so the divergence-catching claim is real, not asserted. 5. **Kill arithmetic — pass.** `applied==0 && cost>0` kills under **L7-K2 with `unitValue` nil** (and is not rescued by a huge denominator); L7-K1 returns `Decided=false` — *undecided, not a pass*; one window never decides; `E4Decay` is active-only. Numerator counts `applied` (a reverted-but-applied record still counts), plus friction tickets that reached Done. 6. **Registry / id discipline — pass** (see above). 7. **Territory — pass.** Only `lab-director/dispatcher/friction/**` (new) + `routines/registry.yaml` (entry + header). No `claims`/`routines` internals beyond the entry; no policy-book edit claimed. ## Non-blocking concerns (no fix required to merge; for the activation PR) - **A closed shadow is not a loadable document.** `Bindings.Revert` sets `Sample = 0` + `VerdictAt`, but `Bindings.Validate` refuses `sample <= 0` (asserted by `TestBindingsRefusals/sample_zero`). So the in-memory revert proof is sound, yet a reverted `context-bindings.json` would be refused by `LoadBindings`. Either allow `sample == 0` when `VerdictAt != nil`, or document "delete the entry on revert" — worth pinning before the file is real. - **Unnamed deviation from the brief:** `filter: q2_derivation` is `true` in the entry, `false` in brief §3.2. `true` is the correct answer given `kind: script` (the loader's q2-true ⇒ code implication), but it is absent from the deviations list. - **The read-before-write half is injected, not implemented:** `Triage.Run` takes the open-fingerprint map from the caller; the reader that resolves "open ticket for this fingerprint" lands with the executor. Fine while `paused`; name it in the activation ticket. - **Count nit:** PR body says "friction: 31 tests"; the package has **30** top-level tests (87 RUN incl. subtests). No functional impact. - **Cross-repo:** `Homelab/pi-extensions#61` still carries 14 `R-28` references in `pi-friction/**` (README, `index.ts`, `ledger.ts`, types, tests) — that companion needs the same re-id to R-30. - **Epic-side (not introduced here, worth a sweep):** the registry header's "Active today (6)" enumerates R-28 (LD4.5b's on-call window) while no R-28 entry exists on `epic/LD4` (census: 5 active). It becomes true when #596 lands. Proceeding to merge on the landing lock (green CI on this head, verdict posted after it, no blocker findings).
aaron merged commit 8d8cbd745b into epic/LD4 2026-10-03 04:08:10 +00:00
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!592
No description provided.