@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.
- package/package.json +2 -1
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +29 -36
- package/src/config/config.mjs +388 -787
- package/src/github/comment-id-guard.mjs +2 -2
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +2 -2
- package/src/loop/gate-fanin.mjs +252 -442
- package/src/loop/handoff-envelope.mjs +19 -19
- package/src/loop/issue-refinement-artifact.mjs +158 -252
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +37 -37
- package/src/loop/queue-board-sync.mjs +16 -55
- package/src/loop/review-dispatch-plan.mjs +60 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +39 -69
- package/src/loop/steering.mjs +16 -68
- package/src/projects/list-queue-items.mjs +16 -146
- package/src/projects/move-queue-item.mjs +15 -141
- package/src/projects/projects-access.mjs +202 -0
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* review-dispatch-plan.mjs — cache-aware review dispatch
|
|
3
|
-
* fingerprinting
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
202
|
-
*
|
|
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
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* `
|
|
211
|
-
*
|
|
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)
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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
|
|
253
|
-
* @param {string[]
|
|
254
|
-
*
|
|
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
|
|
547
|
-
*
|
|
548
|
-
*
|
|
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
|
|
555
|
-
*
|
|
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
|
-
* @
|
|
562
|
-
*
|
|
563
|
-
*
|
|
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
|
|
685
|
+
* 6. Dispatch-prompt layout alignment
|
|
734
686
|
* ------------------------------------------------------------------ */
|
|
735
687
|
|
|
736
|
-
// Leading-bytes capture cap for a dispatched reviewer prompt (
|
|
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
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
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
|
|
776
|
-
* two calls sharing
|
|
777
|
-
*
|
|
778
|
-
*
|
|
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
|
-
*
|
|
785
|
-
*
|
|
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
|
-
*
|
|
790
|
-
* @param {string}
|
|
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
|
|
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
|
|
815
|
-
*
|
|
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
|
|
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 —
|
|
824
|
-
*
|
|
825
|
-
* @param {string} input.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
3
|
-
* composition (issue #1468 slice 5).
|
|
2
|
+
* Additive review-lineage base + per-fix-round delta composition.
|
|
4
3
|
*
|
|
5
|
-
* A
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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
|
|
362
|
+
* Compaction / rebase policy
|
|
388
363
|
* ------------------------------------------------------------------ */
|
|
389
364
|
|
|
390
365
|
/**
|
|
@@ -1,40 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* spec-authority.mjs —
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
//
|
|
275
|
-
// specDigest is never
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
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 (
|
|
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 (
|
|
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
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* `issue-refinement-artifact.mjs`
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
* checklist
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
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
|
-
// (
|
|
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");
|