@dev-loops/core 1.0.1 → 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.angleSuffixnon-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
  /**
@@ -869,6 +807,7 @@ export function verifyPromptLeadingAlignment({ promptLeading, prefixBytes, prefi
869
807
  */
870
808
  export const DEFAULT_DIFF_EXCLUDE_GLOBS = Object.freeze([
871
809
  // Lockfiles.
810
+ "bun.lock", "**/bun.lock",
872
811
  "package-lock.json", "**/package-lock.json",
873
812
  "npm-shrinkwrap.json", "**/npm-shrinkwrap.json",
874
813
  "yarn.lock", "**/yarn.lock",
@@ -982,7 +921,7 @@ function extractDiffBlockPath(blockLines) {
982
921
 
983
922
  /**
984
923
  * Filter a unified diff (`git diff` output) down to the files that should be
985
- * INLINED into a reviewer prompt's shared per-head block (issue #1853):
924
+ * INLINED into a reviewer prompt's shared per-head block:
986
925
  * lockfiles, generated/vendored trees, and any caller-configured
987
926
  * `excludeGlobs` are dropped whole-file (header + all hunks), every other
988
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
  /**