@dev-loops/core 1.0.2-pre.0 → 1.0.2-slim.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +1,13 @@
1
1
  /**
2
- * review-dispatch-plan.mjs — cache-aware review dispatch plan + request-prefix
3
- * fingerprinting + stable/volatile request separation (issue #1468 slices 1-2).
2
+ * review-dispatch-plan.mjs — cache-aware review dispatch: harness capability
3
+ * model, request-prefix fingerprinting, stable/volatile request separation,
4
+ * and the deterministic per-gate-round dispatch-plan builder.
4
5
  *
5
- * This module is the mechanically-checkable foundation for the cache-efficient
6
- * review dispatch design (Section A/B/C/D of the #1468 spec). It owns:
7
- *
8
- * 1. Harness capability model — explicit representation of what a harness can
9
- * observe/control about provider prompt caching
10
- * (breakpointControl / barrierSignal / cacheTtlControl / usageTelemetry).
11
- * Opaque capabilities are represented as such and are NEVER described as
12
- * verified cache hits.
13
- * 2. Request-prefix fingerprinting — a deterministic sha256 over every
14
- * cache-relevant value the dev-loops layer observes or controls (concrete
15
- * model, tool definitions/order, system/project/agent instructions,
16
- * thinking/tool-choice settings, content-block boundaries, shared artifact
17
- * bytes, and breakpoint/TTL intent). Values owned opaquely by a harness are
18
- * represented as a placeholder, never assumed identical.
19
- * 3. Stable/volatile request separation — physically separates stable handoff
20
- * content from volatile `gateState` at the request/artifact boundary, so a
21
- * provider-visible cache boundary sits after the stable materialized
22
- * briefing block and before the late volatile tail + angle suffix.
23
- * 4. Dispatch-plan builder — one deterministic per-gate-round artifact that
24
- * records the complete cache-relevant request shape without duplicating
25
- * briefing content (Section A). `buildAngleRequestGroups` partitions a
26
- * caller's angle -> concrete-model resolutions into that plan's
27
- * `requestGroups` shape, bucketing angles with no override into an
28
- * explicit "inherit" key rather than merging them into a concrete group.
29
- * 5. Primer-form default — deterministic default by harness capability
30
- * (Section C/D): first-output-observable harnesses may let a lead reviewer
31
- * prime; completion-only harnesses default to a short dedicated primer
32
- * unless an adequate TTL is explicit; multiple concrete models partition
33
- * into one primer group per model/request-prefix.
34
- *
35
- * This module is pure and offline: no GitHub, no harness, no clock. All runtime
36
- * execution adapters consume it; it never executes a reviewer itself.
6
+ * Pure and offline: no GitHub, no harness, no clock; runtime execution
7
+ * adapters consume it, it never executes a reviewer itself. A capability a
8
+ * harness owns opaquely (breakpoints/TTL/telemetry it cannot observe) is
9
+ * always represented as opaque/unavailable rather than assumed identical —
10
+ * this module never claims a verified cache hit it cannot prove.
37
11
  */
38
12
  import { createHash } from "node:crypto";
39
13
 
@@ -198,20 +172,16 @@ function isPlainObject(value) {
198
172
  }
199
173
 
200
174
  /**
201
- * Recursively sort object keys for a byte-deterministic serialization (arrays
202
- * keep their order — order is itself cache-relevant for tool definitions and
203
- * content-block boundaries).
175
+ * Recursively sort object keys for byte-deterministic serialization (array
176
+ * order is preserved — order is itself cache-relevant).
204
177
  *
205
- * Trust-boundary validation, refusing loudly rather than silently colliding
206
- * two distinct inputs onto one fingerprint: a non-finite number (NaN/
207
- * Infinity) is rejected rather than let `JSON.stringify` collapse it to
208
- * `null`, a non-plain object (Date/Map/Set/...) is rejected rather than let
209
- * `Object.keys` see it as keyless (and therefore indistinguishable from
210
- * `{}`), and undefined/function/symbol/bigint are rejected rather than
211
- * silently dropped or crash-serialized by `JSON.stringify` itself. The
212
- * accumulator is null-prototype so an own `__proto__` key (a realistic shape
213
- * for JSON.parse'd input) is kept as a plain data property instead of
214
- * vanishing into the prototype chain.
178
+ * Fails closed rather than silently colliding distinct inputs onto one
179
+ * fingerprint: a non-finite number is rejected rather than collapsed to
180
+ * `null`, a non-plain object (Date/Map/Set/...) is rejected rather than
181
+ * treated as keyless, and undefined/function/symbol/bigint are rejected
182
+ * rather than dropped or crash-serialized. The accumulator is null-prototype
183
+ * so an own `__proto__` key stays a plain data property instead of vanishing
184
+ * into the prototype chain.
215
185
  * @param {*} value
216
186
  * @param {string} [keyPath] — dotted path to `value`, for the error message
217
187
  * @returns {*}
@@ -241,24 +211,16 @@ function stableStringify(value, keyPath = "$") {
241
211
  }
242
212
 
243
213
  /**
244
- * Fingerprint the complete observable request prefix (Section A). Every
245
- * cache-relevant value the dev-loops layer observes or controls is folded into
246
- * the hash: concrete model, tool definitions/order, system/project/agent
247
- * instructions, thinking/tool-choice settings, content-block boundaries, shared
248
- * artifact bytes, and breakpoint/TTL intent. Values owned opaquely by a harness
249
- * should be passed as `null`-free opaque markers (see `opaqueMarker`).
214
+ * Fingerprint the complete observable request prefix (Section A): concrete
215
+ * model, tool definitions/order, system/project/agent instructions,
216
+ * thinking/tool-choice settings, content-block boundaries, shared artifact
217
+ * bytes, and breakpoint/TTL intent. Values owned opaquely by a harness should
218
+ * be passed as `null`-free opaque markers (see `opaqueMarker`).
250
219
  *
251
220
  * @param {object} input
252
- * @param {string} input.model - concrete model id for this request group.
253
- * @param {string[]|object[]} [input.tools] - tool definitions/order.
254
- * @param {string|string[]} [input.systemInstructions] - system/project/agent instructions.
255
- * @param {object|string} [input.settings] - thinking/tool-choice settings.
256
- * @param {object[]} [input.contentBlocks] - content-block boundaries + shared bytes.
257
- * @param {string} [input.sharedArtifact] - shared artifact reference (path) or bytes.
258
- * @param {string} [input.cacheBoundary] - e.g. `after_shared_prefix`.
259
- * @param {string} [input.ttlIntent] - one of TTL_INTENT_VALUES.
260
- * @param {string[]} [input.angleSuffix] - angle-specific suffix (excluded from the
261
- * STABLE prefix fingerprint; included here only when invasive).
221
+ * @param {string} input.model - non-empty concrete model id.
222
+ * @param {string[]} [input.angleSuffix] - excluded from the STABLE prefix
223
+ * fingerprint elsewhere, but folded in here when present.
262
224
  * @returns {{ fingerprint: string, canonical: object }}
263
225
  */
264
226
  export function fingerprintRequestPrefix(input) {
@@ -543,30 +505,20 @@ export const INHERIT_MODEL_KEY = "inherit";
543
505
  * input. Angles resolving to the same concrete model id share one group;
544
506
  * angles with no override (`model: null`/`undefined`) form their own explicit
545
507
  * {@link INHERIT_MODEL_KEY} bucket, never merged with a concrete id. An angle
546
- * listed twice with two DIFFERENT models is a caller bug and throws (an angle
547
- * cannot honestly belong to two request groups). A concrete model literally
548
- * named {@link INHERIT_MODEL_KEY} also throws — it would otherwise silently
549
- * collide with the reserved bucket key and become indistinguishable from
550
- * genuine no-override.
508
+ * listed twice with two DIFFERENT models throws (it cannot belong to two
509
+ * request groups); a concrete model literally named {@link INHERIT_MODEL_KEY}
510
+ * also throws, since it would otherwise collide with the reserved bucket key.
551
511
  *
552
512
  * Each group's `requestPrefixFingerprint` is computed via
553
513
  * {@link fingerprintRequestPrefix} over every cache-relevant input this layer
554
- * observes for that group (the bucket's model, tool set/order, instructions,
555
- * settings, content-block boundaries, the shared-prefix bytes, and the
556
- * declared cache boundary/TTL intent) — changing only the angle set within a
557
- * bucket never changes its fingerprint.
514
+ * observes for that group — changing only the angle set within a bucket never
515
+ * changes its fingerprint.
558
516
  *
559
517
  * @param {object} input
560
518
  * @param {Array<{angle: string, model: string|null}>} input.angleModels
561
- * @param {string} [input.sharedPrefixHash] — folded in as the fingerprint's shared-artifact reference.
562
- * @param {Array<string|object>} [input.toolDefinitions] — tool names/definitions in dispatch order
563
- * @param {string|string[]} [input.instructions] — system/project/agent instruction bytes (or a digest)
564
- * @param {object} [input.settings] — thinking/tool-choice settings
565
- * @param {string[]} [input.blockBoundaries] — content-block boundary markers, in order
566
- * @param {string} [input.cacheBoundary]
567
- * @param {string} [input.ttlIntent] — one of TTL_INTENT_VALUES
568
- * @returns {RequestGroup[]} sorted by model (code-unit order, never localeCompare — ICU-dependent
569
- * sorting could order the same two model ids differently across runtimes); angles sorted within a group.
519
+ * @returns {RequestGroup[]} sorted by model (code-unit order, never
520
+ * localeCompare — ICU-dependent sorting could order the same two model ids
521
+ * differently across runtimes); angles sorted within a group.
570
522
  */
571
523
  export function buildAngleRequestGroups({
572
524
  angleModels,
@@ -730,11 +682,11 @@ export function partitionPrimerGroups(requestGroups, capabilities = {}) {
730
682
  }
731
683
 
732
684
  /* ------------------------------------------------------------------ *
733
- * 6. Dispatch-prompt layout alignment (issue #1841, completes #1468)
685
+ * 6. Dispatch-prompt layout alignment
734
686
  * ------------------------------------------------------------------ */
735
687
 
736
- // Leading-bytes capture cap for a dispatched reviewer prompt (issue #1841's
737
- // record-dispatch-prompt-layout.mjs). Sized comfortably above
688
+ // Leading-bytes capture cap for a dispatched reviewer prompt (the
689
+ // record-dispatch-prompt-layout.mjs cap). Sized comfortably above
738
690
  // write-gate-context.mjs's BRIEFING_PREFIX_INLINE_DIFF_CAP_BYTES (200 KiB) so
739
691
  // a full byte-for-byte alignment check never runs out of captured bytes for
740
692
  // an inline-mode round.
@@ -765,33 +717,26 @@ export function renderBriefingPointerLine(prefixPath) {
765
717
  /**
766
718
  * Deterministically compose a full reviewer prompt: the round's
767
719
  * byte-identical invariant prefix INLINED as the leading bytes, followed by
768
- * the (also round-invariant) volatile tail, followed by the per-group angle
769
- * suffix (issue #1852). This is the ONE function every reviewer prompt on the
770
- * canonical fan-out path is built from — never a hand-assembled per-group
771
- * preamble that leads with dynamic prose ahead of the prefix (the
772
- * "angle-first" / pointer-seeding failure mode `verifyPromptLeadingAlignment`
773
- * exists to catch).
720
+ * the round-invariant volatile tail, followed by the per-group angle suffix.
721
+ * This is the ONE function every reviewer prompt on the canonical
722
+ * fan-out path is built from — never a hand-assembled per-group preamble that
723
+ * leads with dynamic prose ahead of the prefix (the "angle-first" failure
724
+ * mode `verifyPromptLeadingAlignment` exists to catch).
774
725
  *
775
- * Byte-identical-prefix-across-groups falls out of the arguments alone: any
776
- * two calls sharing the same `prefixBytes`/`volatileBytes` (true for every
777
- * dispatch unit of one round, since both are round-scoped, not group-scoped)
778
- * produce prompts whose leading span is identical regardless of
779
- * `angleSuffix` — the property AC1 requires, provable by construction rather
780
- * than by review.
726
+ * Byte-identical-prefix-across-groups follows from the arguments alone: any
727
+ * two calls sharing `prefixBytes`/`volatileBytes` (true for every dispatch
728
+ * unit of one round) produce prompts whose leading span is identical
729
+ * regardless of `angleSuffix` — provable by construction, not by review.
781
730
  *
782
731
  * Pure and offline: takes already-read bytes, never reads a file itself (the
783
- * CLI wrapper, `compose-reviewer-prompt.mjs`, owns I/O and the
784
- * record-dispatch-prompt-layout.mjs capture that makes the composed prompt's
785
- * layout binding on `verify-dispatch-prompt-layout.mjs`).
732
+ * CLI wrapper, `compose-reviewer-prompt.mjs`, owns I/O and the capture that
733
+ * makes the composed prompt's layout binding on
734
+ * `verify-dispatch-prompt-layout.mjs`).
786
735
  *
787
736
  * @param {object} input
788
- * @param {string} input.prefixBytes — the round's invariant-prefix bytes
789
- * (`<gate>-<headSha>.briefing-prefix.txt`), non-empty.
790
- * @param {string} [input.volatileBytes] — the round's volatile-tail bytes
791
- * (`<gate>-<headSha>.briefing-volatile.txt`); absent/non-string treated as
792
- * "" (best-effort — a round that never wrote one still composes).
793
- * @param {string} input.angleSuffix — the per-group/angle-specific prompt
794
- * text, non-empty (an empty suffix would compose a prompt naming no work).
737
+ * @param {string} input.prefixBytes — non-empty, the round's invariant-prefix bytes.
738
+ * @param {string} [input.volatileBytes] — round's volatile-tail bytes; absent treated as "".
739
+ * @param {string} input.angleSuffix — non-empty per-group angle-specific prompt text.
795
740
  * @returns {string} the exact full reviewer prompt text.
796
741
  */
797
742
  export function composeReviewerPromptText({ prefixBytes, volatileBytes, angleSuffix } = {}) {
@@ -807,26 +752,19 @@ export function composeReviewerPromptText({ prefixBytes, volatileBytes, angleSuf
807
752
 
808
753
  /**
809
754
  * Decide whether a dispatched reviewer prompt's LEADING bytes are
810
- * cache-aligned (GATE-EXEC-BRIEFING-PREFIX layout, issue #1841): either the
755
+ * cache-aligned (GATE-EXEC-BRIEFING-PREFIX layout): either the
811
756
  * prompt's leading bytes are byte-identical to the round's invariant prefix
812
757
  * (inline mode), or the prompt leads with the byte-identical pointer line
813
758
  * naming the round's invariant-prefix path (pointer-seeding mode), with any
814
- * angle-specific text strictly AFTER it. An angle-first prompt (dynamic
815
- * per-unit prose ahead of the prefix/pointer) matches neither and is
816
- * REJECTED — this is the mechanical proof the prose-only rule lacked.
759
+ * angle-specific text strictly AFTER it. An angle-first prompt matches
760
+ * neither and is REJECTED.
817
761
  *
818
- * Pure and offline: takes the already-captured leading bytes and the already-
819
- * read prefix bytes/path, never reads a file itself (the CLI wrapper owns
820
- * I/O), so this is directly unit-testable with in-memory strings.
762
+ * Pure and offline: takes already-captured bytes, never reads a file itself.
821
763
  *
822
764
  * @param {object} input
823
- * @param {string} input.promptLeading — the captured leading bytes of the
824
- * ACTUAL reviewer prompt (record-dispatch-prompt-layout.mjs's capture).
825
- * @param {string} input.prefixBytes — the round's recorded byte-identical
826
- * invariant-prefix content (the `<gate>-<headSha>.briefing-prefix.txt`
827
- * bytes).
828
- * @param {string} input.prefixPath — the path used to render this round's
829
- * pointer line (must be the SAME path every reviewer was pointed at).
765
+ * @param {string} input.promptLeading — captured leading bytes of the actual reviewer prompt.
766
+ * @param {string} input.prefixBytes — the round's recorded invariant-prefix bytes.
767
+ * @param {string} input.prefixPath — the path used to render this round's pointer line.
830
768
  * @returns {{ aligned: boolean, mode: "inline"|"pointer"|null, reason: string|null }}
831
769
  */
832
770
  export function verifyPromptLeadingAlignment({ promptLeading, prefixBytes, prefixPath } = {}) {
@@ -848,7 +786,7 @@ export function verifyPromptLeadingAlignment({ promptLeading, prefixBytes, prefi
848
786
  }
849
787
 
850
788
  /* ------------------------------------------------------------------ *
851
- * 7. Diff filtering for the shared per-head block (issue #1853)
789
+ * 7. Diff filtering for the shared per-head block
852
790
  * ------------------------------------------------------------------ */
853
791
 
854
792
  /**
@@ -983,7 +921,7 @@ function extractDiffBlockPath(blockLines) {
983
921
 
984
922
  /**
985
923
  * Filter a unified diff (`git diff` output) down to the files that should be
986
- * INLINED into a reviewer prompt's shared per-head block (issue #1853):
924
+ * INLINED into a reviewer prompt's shared per-head block:
987
925
  * lockfiles, generated/vendored trees, and any caller-configured
988
926
  * `excludeGlobs` are dropped whole-file (header + all hunks), every other
989
927
  * file's block passes through byte-for-byte unchanged. Excluding a file here
@@ -1,37 +1,22 @@
1
1
  /**
2
- * review-lineage.mjs — additive review-lineage base + per-fix-round delta
3
- * composition (issue #1468 slice 5).
2
+ * Additive review-lineage base + per-fix-round delta composition.
4
3
  *
5
- * A new head after a fix used to rebuild a full head-specific briefing. This
6
- * module introduces a stable review-lineage base plus deterministic per-round
7
- * delta artifacts, so round 2+ appends only what changed instead of replacing
8
- * the whole context.
9
- *
10
- * Artifact model (Section E of the #1468 spec):
11
- *
12
- * review-lineage-base
13
- * lineage identity + gate + stable contracts/instructions + original
14
- * review target + original full diff.
15
- *
16
- * round-N-delta
17
- * exact base/reviewed SHAs + the fix diff + validation evidence + an
18
- * independent findings verification checklist.
4
+ * A stable review-lineage base plus deterministic per-round delta artifacts let
5
+ * round 2+ append only what changed instead of rebuilding a full head-specific
6
+ * briefing.
19
7
  *
20
8
  * Composition contract:
21
- *
22
- * round-N request = [lineage base][delta 1][delta 2]...[delta N][angle suffix]
9
+ * round-N request = [lineage base][delta 1]...[delta N][angle suffix]
23
10
  *
24
11
  * Composition is append-only and byte-deterministic: the composed request is
25
12
  * the ordered concatenation of the lineage base and the individual delta
26
- * artifacts, never a parse/reserialize of the full PR context as a replacement
27
- * block. Round N+1 appends exactly one new delta segment; every prior segment
28
- * is byte-identical (same ref + same hash) — that is what the
29
- * "does not rebuild the full PR context" test asserts.
13
+ * artifacts, never a parse/reserialize of the full PR context. Round N+1 appends
14
+ * exactly one new delta segment; every prior segment is byte-identical (same
15
+ * ref + same hash).
30
16
  *
31
- * Carry-forward semantics are unchanged: a carried clean angle still records
32
- * its original reviewer and prior head. This module only preserves that
33
- * provenance in the composed request; it does not decide carry-forward (that
34
- * stays in gate-carry-forward.mjs) and it never fabricates a verdict.
17
+ * Carry-forward provenance (original reviewer + prior head of a carried clean
18
+ * angle) is preserved in the composed request but not decided here (that stays
19
+ * in gate-carry-forward.mjs); a verdict is never fabricated.
35
20
  *
36
21
  * This module is pure and offline: no GitHub, no harness, no clock.
37
22
  */
@@ -226,26 +211,16 @@ export const ANGLE_SUFFIX_SLOT = "angleSuffix";
226
211
 
227
212
  /**
228
213
  * Compose the round-N request as an append-only ordered segment list:
229
- *
230
214
  * [lineage base][delta 1]...[delta N][angle suffix]
231
215
  *
232
- * The returned `segments` carry individual artifact bytes + hashes so a
233
- * consumer can render the request by concatenating segment bytes IN ORDER
234
- * (never parsing/reserializing prior segments). Round N+1 appends exactly one
235
- * new delta segment: callers should REUSE the prior composed segments (or the
236
- * base + prior deltas) rather than rebuilding the full PR context, and this
237
- * function's contract + tests pin that reuse property.
238
- *
239
- * Segments before the new delta are byte-identical to the prior round's
240
- * (same `ref` + same `hash`), which is the mechanical proof of append-only
241
- * composition (AC-2: does not rebuild the full PR context as a replacement
242
- * block).
216
+ * Returned `segments` carry individual artifact bytes + hashes; a consumer
217
+ * renders the request by concatenating segment bytes IN ORDER. Round N+1 appends
218
+ * exactly one new delta segment and every earlier segment is byte-identical to
219
+ * the prior round's (same `ref` + same `hash`).
243
220
  *
244
- * `carriedAngles` provenance is preserved unchanged (carry-forward semantics):
245
- * each entry is { angle, originalReviewer, priorHead } and is folded into the
246
- * composed hash so a carried angle's provenance is pinned — but never
247
- * fabricated. A carried clean angle keeps exactly the original reviewer and
248
- * prior head it was recorded with.
221
+ * `carriedAngles` provenance is preserved unchanged: each entry
222
+ * { angle, originalReviewer, priorHead } is folded into the composed hash so a
223
+ * carried angle's provenance is pinned, never fabricated.
249
224
  *
250
225
  * @param {object} input
251
226
  * @param {object} input.lineageBase - a valid review-lineage-base artifact.
@@ -384,7 +359,7 @@ export function renderComposedRequest(composed) {
384
359
  }
385
360
 
386
361
  /* ------------------------------------------------------------------ *
387
- * Compaction / rebase policy (issue #1468 slice 6)
362
+ * Compaction / rebase policy
388
363
  * ------------------------------------------------------------------ */
389
364
 
390
365
  /**
@@ -1,40 +1,26 @@
1
1
  /**
2
- * spec-authority.mjs — the canonical shared contract surface for IMMUTABLE SPEC
3
- * AUTHORITY across the review / judge / fixer / gate / re-entry pipeline.
2
+ * spec-authority.mjs — canonical shared contract for IMMUTABLE SPEC AUTHORITY
3
+ * across the review / judge / fixer / gate / re-entry pipeline. Pure and
4
+ * side-effect free; persistence is the caller's job.
4
5
  *
5
- * This module is PURE and side-effect free. It owns, in one place (so no harness
6
- * prompt has to re-state normative rules that could drift):
7
- *
8
- * 1. The two INDEPENDENT revision identities every decision must pin:
9
- * - `specDigest` — a deterministic digest of the normalized canonical
10
- * tracker AC / DoD / Non-goals. It identifies what the work is REQUIRED
11
- * and FORBIDDEN to do. It is NEVER derived from a head SHA.
12
- * - reviewed implementation revision — `headSha` plus a `contentDigest`.
13
- * It identifies what the reviewer actually evaluated. A new head/content
14
- * digest NEVER masquerades as a spec change.
15
- *
16
- * 2. The four named, machine-readable judge disposition outcomes. For every
17
- * finding the judge evaluates the finding AND each proposed remediation
18
- * against the COMPLETE spec (not one supportive criterion) and selects
19
- * exactly one outcome.
20
- *
21
- * 3. Autonomous vs last-resort escalation: only a `spec_cannot_decide` outcome
22
- * routes to a human-spec-decision state. A finding/remediation conflict
23
- * resolves autonomously (reject the finding, or reject the remedy and route
24
- * to a compliant alternative).
25
- *
26
- * 4. Human-only spec change: a material spec change/reinterpretation produces a
27
- * NEW `specDigest`; every approval/disposition/gate result derived from the
28
- * prior digest is stale and must be re-established.
29
- *
30
- * 5. Criterion-scoped invalidation: a fixer push stales only the approvals for
31
- * the criteria whose covered content it changed; unaffected criteria carry
32
- * forward ONLY with positive deterministic proof that both their governing
33
- * spec text and their covered surface are unchanged. Unknown impact fails
34
- * closed to fresh review.
35
- *
36
- * Persistence is the caller's job; this module validates and decides, it never
37
- * reads or writes files.
6
+ * Owns, in one place so no harness prompt re-states rules that could drift:
7
+ * 1. Two INDEPENDENT revision identities every decision must pin: `specDigest`
8
+ * (what the work is REQUIRED/FORBIDDEN to do — never derived from a head
9
+ * SHA) and the reviewed revision (`headSha` + `contentDigest`, what was
10
+ * actually evaluated — never masquerades as a spec change).
11
+ * 2. The four named judge disposition outcomes; the judge evaluates every
12
+ * finding AND its proposed remediation against the COMPLETE spec (not one
13
+ * supportive criterion) and selects exactly one.
14
+ * 3. Autonomous vs last-resort escalation: only `spec_cannot_decide` routes to
15
+ * a human-spec-decision state; a finding/remediation conflict resolves
16
+ * autonomously.
17
+ * 4. Human-only spec change: a material spec change produces a NEW
18
+ * `specDigest`, staling every prior-derived approval/disposition/gate
19
+ * result.
20
+ * 5. Criterion-scoped invalidation: a fixer push stales only the approvals for
21
+ * criteria whose covered content it changed; an unaffected criterion
22
+ * carries forward only with positive proof both its spec text and covered
23
+ * surface are unchanged — unknown impact fails closed.
38
24
  */
39
25
 
40
26
  import { sha256Hex } from "./review-dispatch-plan.mjs";
@@ -271,13 +257,11 @@ export function buildRevisionIdentity({ spec, specDigest, headSha, content, cont
271
257
  if (resolvedSpecDigest === resolvedContentDigest) {
272
258
  throw new Error("SPEC-AUTHORITY-REVISION-IDENTITIES: specDigest and contentDigest must be distinct identities (fail closed)");
273
259
  }
274
- // The domain separator in computeSpecDigest is the structural guarantee that a
275
- // specDigest is never derivable from a head SHA. This is the defensive tripwire
276
- // for an explicitly-SUPPLIED digest: reject when the digest's hex body equals or
277
- // embeds the head SHA (any length), not only the 64-hex exact case — the
278
- // exact-equality-only form was a no-op for a normal 40-hex Git SHA. The
279
- // false-positive probability of a real spec digest incidentally embedding the
280
- // head SHA is negligible, and the fail-closed direction is safe.
260
+ // Defensive tripwire for an explicitly-SUPPLIED digest: the domain separator
261
+ // in computeSpecDigest already guarantees specDigest is never DERIVED from a
262
+ // headSha, but reject here too when the digest's hex body equals or embeds
263
+ // the head SHA (any length) — fail-closed, since a real spec digest
264
+ // incidentally embedding a head SHA is negligible.
281
265
  if (resolvedSpecDigest.slice("sha256:".length).includes(sha)) {
282
266
  throw new Error("SPEC-AUTHORITY-REVISION-IDENTITIES: specDigest must not be derived from or embed headSha (fail closed)");
283
267
  }
@@ -570,7 +554,7 @@ export function resolveCriterionInvalidation({
570
554
  }
571
555
 
572
556
  /**
573
- * AC1 (issue 2008 / ADR 0061): the ONE shared identity-stamp helper every
557
+ * AC1 (ADR 0061): the ONE shared identity-stamp helper every
574
558
  * gate/fixer record writer threads its revision identity + checked criteria
575
559
  * through, so the writers cannot drift from independently-recomputed fields.
576
560
  * Validates the pinned trio (reusing {@link assertDigestShape} /
@@ -609,7 +593,7 @@ export function stampSpecAuthorityIdentity(record, { specDigest, headSha, conten
609
593
  }
610
594
 
611
595
  // ---------------------------------------------------------------------------
612
- // AC7 (issue 2008 / ADR 0061): pure affected-criteria producer
596
+ // AC7 (ADR 0061): pure affected-criteria producer
613
597
  // ---------------------------------------------------------------------------
614
598
 
615
599
  // ponytail: minimal glob subset — exact path, a `dir/**` prefix, or a single
@@ -704,30 +688,16 @@ export function resolveAffectedCriteria({ changedPaths, criterionCoverage } = {}
704
688
  * raw (un-normalized) lists; pass the result to {@link computeSpecDigest} /
705
689
  * {@link normalizeSpec}.
706
690
  *
707
- * ponytail (#2016 root cause fix): the AC/DoD source is the authoritative
708
- * AC→DoD mapping MATRIX (`detectAcDodMatrix`, reused byte-identical from
709
- * `issue-refinement-artifact.mjs` — no second matrix parser) when the body
710
- * carries one that parses as valid; the list-form AC/DoD checklists
711
- * (`extractChecklistItems`) are a REDUNDANT presentation projection of that
712
- * same matrix (`derivePrChecklistsFromIssueMatrix` derives the PR-side
713
- * checklist from it) and are read only as the fail-closed fallback for older
714
- * issue bodies that carry no matrix at all. This is the fix for the reported
715
- * bug: adding/removing/re-heading a checklist alias that projects an
716
- * UNCHANGED matrix no longer touches `specDigest`, because the checklist is
717
- * no longer part of the hashed input once a valid matrix exists. Any edit
718
- * that changes the matrix itself — a criterion's text, its completion-
719
- * evidence cell, or the row set (add/remove) — changes `matrix.rows` and
720
- * therefore still changes the digest (fail-closed: no genuine spec change is
721
- * exempted). Non-goals are unaffected by this change: they were never part of
722
- * the redundant-checklist problem (the matrix does not carry Non-goals), so
723
- * they stay sourced from the `## Non-goals` section exactly as before.
724
- *
725
- * Fail-closed fallback: when `detectAcDodMatrix` reports `found: false` (no
726
- * matrix at all) or `valid: false` (empty/malformed/identifier-only table —
727
- * i.e. it cannot be positively parsed as a real semantic mapping), this falls
728
- * back to the PRE-#2016 behavior of hashing the extracted checklist text
729
- * verbatim. A body this function cannot prove carries an equivalent matrix
730
- * must never silently narrow what gets digested.
691
+ * The AC/DoD source is the authoritative AC→DoD mapping MATRIX
692
+ * (`detectAcDodMatrix`, reused byte-identical from
693
+ * `issue-refinement-artifact.mjs`) when the body carries one that parses as
694
+ * valid; the list-form checklists (`extractChecklistItems`) are a redundant
695
+ * presentation projection of that same matrix and are read only as the
696
+ * fail-closed fallback for older issue bodies with no matrix at all.
697
+ * A checklist-only edit that projects an unchanged matrix therefore never
698
+ * touches `specDigest`; any edit that changes the matrix itself still does.
699
+ * Non-goals always come from the `## Non-goals` section — the matrix does not
700
+ * carry them.
731
701
  *
732
702
  * @param {string} body — the tracker issue markdown body
733
703
  * @returns {{ acceptanceCriteria: string[], definitionOfDone: string[], nonGoals: string[] }}
@@ -747,7 +717,7 @@ export function extractSpecFromBody(body) {
747
717
  }
748
718
 
749
719
  // Fallback: no positively-parseable matrix — hash the checklist projection
750
- // (pre-#2016 behavior) rather than an empty/weaker AC/DoD surface.
720
+ // (prior behavior) rather than an empty/weaker AC/DoD surface.
751
721
  const acSection = extractSection(body, "Acceptance criteria");
752
722
  const dodSection =
753
723
  extractSection(body, "Definition of done") ?? extractSection(body, "DoD");