@sema-agent/core 5.64.0 → 6.0.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.
Files changed (165) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +113 -4
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +48 -8
  35. package/dist/core/hooks.js +39 -22
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +6 -0
  45. package/dist/core/memory-engine/engine.d.ts +329 -15
  46. package/dist/core/memory-engine/engine.js +364 -34
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +80 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/remote-env.d.ts +3 -3
  71. package/dist/core/retention-policy.d.ts +9 -0
  72. package/dist/core/retention-policy.js +5 -2
  73. package/dist/core/retention.d.ts +13 -2
  74. package/dist/core/runner/assemble-result.d.ts +19 -1
  75. package/dist/core/runner/assemble-result.js +17 -2
  76. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  77. package/dist/core/runner/compaction-call-options.js +3 -0
  78. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  79. package/dist/core/runner/memory-capture-optout.js +53 -0
  80. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  81. package/dist/core/runner/prepare-config-doors.js +16 -0
  82. package/dist/core/runner/prepare-hands-readface.d.ts +119 -5
  83. package/dist/core/runner/prepare-hands-readface.js +103 -8
  84. package/dist/core/runner/prepare-memory.d.ts +88 -0
  85. package/dist/core/runner/prepare-memory.js +306 -25
  86. package/dist/core/runner/prepare-task.d.ts +156 -5
  87. package/dist/core/runner/prepare-task.js +488 -98
  88. package/dist/core/runner/runtask.d.ts +27 -20
  89. package/dist/core/runner/runtask.js +283 -99
  90. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  91. package/dist/core/runner/session-file-state-replay.js +52 -1
  92. package/dist/core/runner/tool-disclosure.js +2 -1
  93. package/dist/core/runner/turn-attachments.d.ts +22 -12
  94. package/dist/core/session-store.d.ts +1 -1
  95. package/dist/core/session-store.js +6 -1
  96. package/dist/core/session.d.ts +34 -1
  97. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  98. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  99. package/dist/core/stub-env.d.ts +4 -0
  100. package/dist/core/stub-env.js +1 -0
  101. package/dist/core/task-registry-shared.js +30 -2
  102. package/dist/core/tool-errors.js +1 -0
  103. package/dist/core/tool-policy.d.ts +172 -1
  104. package/dist/core/tool-policy.js +32 -1
  105. package/dist/core/tool-result-store.js +2 -1
  106. package/dist/core/trace.d.ts +24 -0
  107. package/dist/core/types.d.ts +875 -97
  108. package/dist/core/types.js +4 -3
  109. package/dist/core/untrusted-text.d.ts +1 -1
  110. package/dist/core/untrusted-text.js +8 -0
  111. package/dist/core/workflow-run-store-contract.js +17 -0
  112. package/dist/core/workflow-run-store.d.ts +20 -0
  113. package/dist/core/workflow-run-store.js +1 -0
  114. package/dist/engine/compaction/compaction.d.ts +88 -10
  115. package/dist/engine/compaction/compaction.js +109 -30
  116. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  117. package/dist/engine/execution-env/node-execution-env.js +28 -0
  118. package/dist/engine/harness/agent-harness.d.ts +52 -1
  119. package/dist/engine/harness/agent-harness.js +36 -1
  120. package/dist/engine/harness/types.d.ts +44 -1
  121. package/dist/engine/llm/types.d.ts +50 -4
  122. package/dist/engine/loop/agent-loop.d.ts +5 -1
  123. package/dist/engine/loop/agent-loop.js +25 -0
  124. package/dist/engine/loop/types.d.ts +19 -0
  125. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  126. package/dist/engine/session/session.js +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -6
  129. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  130. package/dist/orchestration/run-workflow-tool.js +22 -3
  131. package/dist/orchestration/workflow-governance.d.ts +59 -1
  132. package/dist/orchestration/workflow-governance.js +61 -8
  133. package/dist/orchestration/workflow-meta.d.ts +4 -2
  134. package/dist/orchestration/workflow-primitives.js +56 -13
  135. package/dist/orchestration/workflow-types.d.ts +112 -1
  136. package/dist/orchestration/workflow-types.js +2 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +182 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +15 -7
  141. package/dist/prompts/default.js +3 -0
  142. package/dist/stores/file/file-history-store.d.ts +368 -0
  143. package/dist/stores/file/file-history-store.js +1248 -0
  144. package/dist/stores/file/index.d.ts +22 -13
  145. package/dist/stores/file/index.js +4 -4
  146. package/dist/stores/file/permission-rule-store.js +1 -0
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +159 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -490,6 +490,20 @@ export declare function deriveProductSlug(name: string | undefined, fallback: st
490
490
  export declare class ConsolidationRefusedError extends Error {
491
491
  readonly code: string;
492
492
  readonly reasons: string[];
493
+ /**
494
+ * #495 — THIS refusal's own occurrence identity, minted where the fact is created (one throw, one
495
+ * id) rather than at whatever face later describes it. The notice dialect
496
+ * ({@link import("./engine.js").memoryConsolidationRefusedNotice}) reads it off the error, so the
497
+ * throw and its announcement name the same event however many times a host renders it: a repeated
498
+ * gate refusal (same `code`, same `scope`, byte-identical `message`) is otherwise indistinguishable
499
+ * from one refusal logged twice.
500
+ *
501
+ * Per-occurrence by construction, and correctly so: a refusal is a pure in-process event with no
502
+ * durable seat — nothing persists it, so nothing replays it, and two refusals ARE two facts. (The
503
+ * fail-open shape #476 rejected is a per-DELIVERY mint for families whose occurrences do return
504
+ * from a durable store and must answer the same id each time; this is the opposite geometry.)
505
+ */
506
+ readonly occurrenceId: string;
493
507
  constructor(code: string, message: string, reasons?: string[]);
494
508
  }
495
509
  /** A NotePatch list for one frozen product (the §1.3 add-only arm's one spelling). */
@@ -526,16 +540,25 @@ export declare function productAddPatch(product: MemoryEntry): NotePatch;
526
540
  * influence); committed cycles stand (add-only, no rollback), and the host
527
541
  * re-runs to re-mint over the current bytes (immediately or next window —
528
542
  * host policy).
543
+ * - `purity_violation` — design/383 §1.3 (S-1): the minted plan carries a product that will
544
+ * commit UNMARKED while containing bytes only the MARKED arm was shown. The
545
+ * exposure partition's isolation premise is falsified, so the whole mint is
546
+ * discarded with ZERO writes and the transcript is archived (a security
547
+ * event's only evidence). DELIBERATELY DISTINCT from `driver_failed`: that
548
+ * one says the model half could not finish, this one says the safety
549
+ * topology did not hold — a host that folds them loses the ability to tell
550
+ * "the model was flaky" from "content crossed a boundary it must not cross",
551
+ * and only the second is an incident.
529
552
  * Extending this set is a consumer-visible closed-set expansion (release-note duty: every
530
553
  * downstream that pinned the enumeration becomes a named party — `mint_invalidated` was the
531
- * design/376-C1 add, named on its release).
554
+ * design/376-C1 add, `purity_violation` the design/383 片1 add, each named on its release).
532
555
  *
533
556
  * The type DERIVES from the array (never `satisfies` beside it): the array exists precisely for
534
557
  * downstream enumeration, and two independent declarations would let a new union arm skip the
535
558
  * array with no compile error — the derived form makes an edit to either side a type error at
536
559
  * the other.
537
560
  */
538
- export declare const CONSOLIDATION_RUN_STOP_REASONS: readonly ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated"];
561
+ export declare const CONSOLIDATION_RUN_STOP_REASONS: readonly ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated", "purity_violation"];
539
562
  /** See the doc on {@link CONSOLIDATION_RUN_STOP_REASONS} — the single source this derives from. */
540
563
  export type ConsolidationRunStopReason = (typeof CONSOLIDATION_RUN_STOP_REASONS)[number];
541
564
  export {};
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, renameSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { uuidv7 } from "../../engine/session/uuid.js";
3
4
  import { atomicWriteFileSync, lockedStrictUpdate, readStrictSidecar, ControlPlaneCorruptError, ensureDirExists } from "./layout.js";
4
5
  export function deriveSupersededSet(headers, opts = {}) {
5
6
  const byId = new Map();
@@ -353,13 +354,15 @@ export function deriveProductSlug(name, fallback) {
353
354
  export class ConsolidationRefusedError extends Error {
354
355
  code;
355
356
  reasons;
357
+ occurrenceId;
356
358
  constructor(code, message, reasons = []) {
357
359
  super(reasons.length > 0 ? `${message}\n${reasons.map((r) => ` - ${r}`).join("\n")}` : message);
358
360
  this.code = code;
359
361
  this.reasons = reasons;
362
+ this.occurrenceId = uuidv7();
360
363
  }
361
364
  }
362
365
  export function productAddPatch(product) {
363
366
  return { op: "add", id: product.id, entry: product, guard: "absent" };
364
367
  }
365
- export const CONSOLIDATION_RUN_STOP_REASONS = ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated"];
368
+ export const CONSOLIDATION_RUN_STOP_REASONS = ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated", "purity_violation"];
@@ -380,12 +380,95 @@ export declare const normalizedCandidateRev: (rev: unknown) => string;
380
380
  * transcript labels — byte-identical to the plain mint modulo the attestation echo and the
381
381
  * roster (C1-G1's identity claim).
382
382
  * - Either arm failing fails the WHOLE mint (one mint, one verdict; both arms' calls ride the
383
- * failure transcript). This widens nothing: under the run-level mint a hostile marked row
383
+ * failure transcript). This widens nothing: under the run-level mint an adversarial marked row
384
384
  * already sat inside the single grouping call and could fail it whole.
385
385
  * - Cross-arm group structure is impossible by construction: each arm's `sanitizeLlmGroups`
386
386
  * valid map is built over that arm's own 1..N numbering.
387
387
  */
388
388
  export declare function mintExposurePartitionedPlan(args: Parameters<typeof mintLlmConsolidationPlan>[0]): Promise<MintLlmConsolidationPlanResult>;
389
+ /**
390
+ * The detector's frozen algorithm parameters — a CONTRACT SIBLING, published beside the readings
391
+ * so a run's verdict can be re-derived from the archive alone, and deliberately NOT a knob: this
392
+ * is a safety gate, and a safety gate with an off switch is a safety gate that gets switched off
393
+ * under deadline pressure. A new parameter set is a NEW `version`, exactly like the prompt
394
+ * contract's own rule.
395
+ */
396
+ export interface MemoryDistillerPurityContract {
397
+ version: string;
398
+ /** Unicode normalization applied to BOTH sides before comparison (so a decomposed copy of a
399
+ * composed original still compares equal). */
400
+ normalization: "NFC";
401
+ /** Case is PRESERVED. Case-folding would widen the detector toward paraphrase matching and start
402
+ * minting false positives on ordinary prose — this gate answers "were these bytes copied", and
403
+ * a case change is already outside "verbatim". */
404
+ caseFold: false;
405
+ /** Shingle width, in whitespace-separated words: the comparison unit on every side. */
406
+ shingleWords: number;
407
+ /** A shingle shorter than this many characters is dropped before comparison (the floor that
408
+ * keeps short, high-frequency phrases out of the evidence set). */
409
+ minShingleChars: number;
410
+ }
411
+ export declare const MEMORY_DISTILLER_PURITY_CONTRACT_V1: MemoryDistillerPurityContract;
412
+ /** One witness: a marked-side-unique shingle found verbatim in a clean product's own bytes. */
413
+ export interface CleanArmLeakFinding {
414
+ /** The product's INDEX in `plan.products` — the locator safe to put in a message. Unlike
415
+ * {@link productKey} it is harness-assigned, so it cannot itself carry a payload. */
416
+ productIndex: number;
417
+ /** The product's key AS THE MODEL WROTE IT (`sanitizeLlmGroups` passes `String(g.key)` through
418
+ * unbounded). Useful when reading the archive; NEVER safe to interpolate into a notice, an
419
+ * announcement or a log line that reaches a model context — the key is model-authored text and
420
+ * can carry the same payload the shingle does. Use {@link productIndex} there. */
421
+ productKey: string;
422
+ field: "name" | "description" | "body";
423
+ /** The offending shingle VERBATIM — the evidence, not a description of it. Same rule as
424
+ * {@link productKey}: this is for the archive and an operator's own eyes, never for a message
425
+ * that lands in front of a model. */
426
+ shingle: string;
427
+ }
428
+ export interface CleanArmLeakVerdict {
429
+ /** False ⇔ the gate had nothing to check (see {@link notEngagedReason}); never a pass claim. */
430
+ engaged: boolean;
431
+ notEngagedReason?: string;
432
+ /** Size of the marked-side-unique evidence set (clean-arm bytes already subtracted). */
433
+ markedUniqueShingles: number;
434
+ /** How many products were scanned (the all-clean-input products of a partitioned plan). */
435
+ scannedProducts: number;
436
+ findings: CleanArmLeakFinding[];
437
+ }
438
+ /**
439
+ * design/383 §1.3 (S-1) — a VERBATIM-LEAK DETECTOR over a minted exposure-partitioned plan. Pure
440
+ * bytes, zero model calls, run in the driver between the mint and the plan cache.
441
+ *
442
+ * WHAT IT ASKS: did any byte sequence that ONLY the marked arm was shown come back out inside a
443
+ * product that will commit UNMARKED? The prompt-side audit (C1-G5) proves the clean arm's prompt
444
+ * carried no marked bytes; this proves the partition machine itself was not bypassed, mis-wired or
445
+ * side-channelled — a different failure surface, which is why both gates ship.
446
+ *
447
+ * HOW:
448
+ * 1. evidence source = the MARKED arm's verbatim grouping prompt (`minting.arms[marked]`), which
449
+ * is the exact rendered form the model saw — `name — description\n body`. Reading only entry
450
+ * bodies would miss a payload parked in a `description`;
451
+ * 2. minus the CLEAN arm's rendered bytes ⇒ the MARKED-SIDE-UNIQUE set. Text both sides legitimately
452
+ * carry (the shared instruction template, a description a clean sibling also holds) proves
453
+ * nothing and is subtracted here — this step is what keeps the gate off false positives;
454
+ * 3. scanned surface = the all-clean-input products' `name` / `description` / `body`. "Clean" is
455
+ * read off the plan's own served roster (no marked input id), which is the same judgment that
456
+ * decides the product commits without an origin marker. A plan that claims the attestation but
457
+ * carries NO roster (no plan this driver mints, but a foreign minter could stamp one) has every
458
+ * product read as clean — including the marked arm's own, which legitimately holds marked
459
+ * bytes — so it refuses. That is the intended direction: an attestation with no evidence behind
460
+ * it should not buy a plan the per-product fold;
461
+ * 4. any intersection ⇒ the caller fails the mint WHOLE. A leak falsifies the clean arm's
462
+ * isolation premise, and the other products of the same mint rest on that same premise.
463
+ *
464
+ * HONEST BOUNDARY (do not let this JSDoc be read as more than it is): this catches VERBATIM
465
+ * copying. It does not catch paraphrase, translation, or a summary of marked content — a complete
466
+ * detector would need a semantic judge, i.e. a model judging a model, which this design forbids.
467
+ * A leak shorter than one shingle (see the contract's `shingleWords`/`minShingleChars`) is below
468
+ * the floor and will not be seen. It is a leak DETECTOR, not a purity PROOF; `engaged: false` is
469
+ * "nothing to check here", never "checked and clean".
470
+ */
471
+ export declare function detectCleanArmVerbatimLeak(plan: LlmConsolidationPlan, contract?: MemoryDistillerPurityContract): CleanArmLeakVerdict;
389
472
  /** The distiller signature the drive consumes: `(candidates, snapshot, scope) -> proposal`.
390
473
  * At runtime only `entry.id` is load-bearing for the replay filter — test/bench stubs that hand
391
474
  * partial entry shapes are tolerated by the reads, the type states the engine's real snapshot.
@@ -459,6 +459,74 @@ export async function mintExposurePartitionedPlan(args) {
459
459
  },
460
460
  };
461
461
  }
462
+ export const MEMORY_DISTILLER_PURITY_CONTRACT_V1 = Object.freeze({
463
+ version: "pu-1",
464
+ normalization: "NFC",
465
+ caseFold: false,
466
+ shingleWords: 6,
467
+ minShingleChars: 24,
468
+ });
469
+ function purityContractRefusal(message) {
470
+ const e = new Error(message);
471
+ e.code = "config.memory_distiller_purity_contract";
472
+ return e;
473
+ }
474
+ function screenPurityContract(c) {
475
+ if (typeof c?.version !== "string" || c.version.trim() === "")
476
+ throw purityContractRefusal("the distiller purity contract needs a non-empty version — the verdict is published beside readings and must name the parameters it was taken under.");
477
+ if (c.normalization !== "NFC")
478
+ throw purityContractRefusal(`the distiller purity contract's normalization must be "NFC" (got ${JSON.stringify(c.normalization)}) — the two sides must normalize identically or a decomposed copy reads as clean.`);
479
+ if (c.caseFold !== false)
480
+ throw purityContractRefusal(`the distiller purity contract's caseFold must be false (got ${JSON.stringify(c.caseFold)}) — see the field's own doc.`);
481
+ if (!Number.isInteger(c.shingleWords) || c.shingleWords < 2)
482
+ throw purityContractRefusal(`the distiller purity contract's shingleWords must be an integer ≥ 2 (got ${String(c.shingleWords)}) — a shorter unit is a word-frequency match, not a copy witness.`);
483
+ if (!Number.isInteger(c.minShingleChars) || c.minShingleChars < 1)
484
+ throw purityContractRefusal(`the distiller purity contract's minShingleChars must be a positive integer (got ${String(c.minShingleChars)}) — a zero floor admits whitespace-only shingles as evidence.`);
485
+ return c;
486
+ }
487
+ function shinglesOf(text, c) {
488
+ const words = text.normalize(c.normalization).split(/\s+/).filter((w) => w !== "");
489
+ const out = new Set();
490
+ for (let i = 0; i + c.shingleWords <= words.length; i++) {
491
+ const s = words.slice(i, i + c.shingleWords).join(" ");
492
+ if (s.length >= c.minShingleChars)
493
+ out.add(s);
494
+ }
495
+ return out;
496
+ }
497
+ export function detectCleanArmVerbatimLeak(plan, contract = MEMORY_DISTILLER_PURITY_CONTRACT_V1) {
498
+ const c = screenPurityContract(contract);
499
+ const none = (notEngagedReason) => ({ engaged: false, notEngagedReason, markedUniqueShingles: 0, scannedProducts: 0, findings: [] });
500
+ if (plan.mintExposure !== "partitioned")
501
+ return none("the plan carries no exposure-partition attestation — there are no arms to compare (an un-attested plan folds run-level, where every product is marked anyway)");
502
+ const arms = plan.minting.arms ?? [];
503
+ const markedArm = arms.find((a) => a.arm === "marked");
504
+ const cleanArm = arms.find((a) => a.arm === "clean");
505
+ if (markedArm === undefined)
506
+ return none("no marked arm was minted — no marked entry's bytes were rendered into any call");
507
+ if (cleanArm === undefined)
508
+ return none("no clean arm was minted — every product of this plan carries a marked input and commits marked");
509
+ const cleanPromptShingles = shinglesOf(cleanArm.groupingPrompt, c);
510
+ const markedUnique = new Set();
511
+ for (const s of shinglesOf(markedArm.groupingPrompt, c))
512
+ if (!cleanPromptShingles.has(s))
513
+ markedUnique.add(s);
514
+ const markedInputIds = new Set((plan.servedCandidates ?? []).filter((r) => r.marked).map((r) => r.id));
515
+ const cleanProducts = plan.products.map((p, productIndex) => ({ p, productIndex })).filter(({ p }) => !(p.inputIds ?? []).some((id) => markedInputIds.has(id)));
516
+ const findings = [];
517
+ for (const { p, productIndex } of cleanProducts) {
518
+ for (const field of ["name", "description", "body"]) {
519
+ const text = p[field];
520
+ if (typeof text !== "string" || text === "")
521
+ continue;
522
+ for (const s of shinglesOf(text, c)) {
523
+ if (markedUnique.has(s))
524
+ findings.push({ productIndex, productKey: p.key, field, shingle: s });
525
+ }
526
+ }
527
+ }
528
+ return { engaged: true, markedUniqueShingles: markedUnique.size, scannedProducts: cleanProducts.length, findings };
529
+ }
462
530
  export function llmPlanDistiller(plan) {
463
531
  const products = plan?.products ?? [];
464
532
  const mintExposure = plan?.mintExposure === "partitioned" ? "partitioned" : undefined;
@@ -63,6 +63,12 @@ export function mergeHarvestReports(a, b) {
63
63
  restored: [...a.restored, ...b.restored],
64
64
  warnings: [...a.warnings, ...b.warnings],
65
65
  };
66
+ const reportId = a.reportId ?? b.reportId;
67
+ if (reportId !== undefined)
68
+ merged.reportId = reportId;
69
+ const parts = [...new Set([a.reportId, b.reportId].filter((x) => x !== undefined))];
70
+ if (parts.length > 1)
71
+ merged.mergedFrom = parts;
66
72
  const incident = a.incident ?? b.incident;
67
73
  if (incident !== undefined)
68
74
  merged.incident = incident;