@loomwork/plugin-content 0.0.0-stage → 0.1.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 (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +92 -2
  4. package/dist/CachingContentStore.d.ts +54 -0
  5. package/dist/CachingContentStore.d.ts.map +1 -0
  6. package/dist/CachingContentStore.js +171 -0
  7. package/dist/CachingContentStore.js.map +1 -0
  8. package/dist/ContentResolvingEventStore.d.ts +70 -0
  9. package/dist/ContentResolvingEventStore.d.ts.map +1 -0
  10. package/dist/ContentResolvingEventStore.js +245 -0
  11. package/dist/ContentResolvingEventStore.js.map +1 -0
  12. package/dist/ContentStore.d.ts +186 -0
  13. package/dist/ContentStore.d.ts.map +1 -0
  14. package/dist/ContentStore.js +177 -0
  15. package/dist/ContentStore.js.map +1 -0
  16. package/dist/ContentTouchingSnapshotStore.d.ts +56 -0
  17. package/dist/ContentTouchingSnapshotStore.d.ts.map +1 -0
  18. package/dist/ContentTouchingSnapshotStore.js +85 -0
  19. package/dist/ContentTouchingSnapshotStore.js.map +1 -0
  20. package/dist/citations.d.ts +40 -0
  21. package/dist/citations.d.ts.map +1 -0
  22. package/dist/citations.js +118 -0
  23. package/dist/citations.js.map +1 -0
  24. package/dist/content.bench.d.ts +2 -0
  25. package/dist/content.bench.d.ts.map +1 -0
  26. package/dist/content.bench.js +75 -0
  27. package/dist/content.bench.js.map +1 -0
  28. package/dist/content.d.ts +40 -0
  29. package/dist/content.d.ts.map +1 -0
  30. package/dist/content.js +177 -0
  31. package/dist/content.js.map +1 -0
  32. package/dist/derive.d.ts +9 -0
  33. package/dist/derive.d.ts.map +1 -0
  34. package/dist/derive.js +40 -0
  35. package/dist/derive.js.map +1 -0
  36. package/dist/errors.d.ts +6 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +20 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/executionEnd.d.ts +22 -0
  41. package/dist/executionEnd.d.ts.map +1 -0
  42. package/dist/executionEnd.js +66 -0
  43. package/dist/executionEnd.js.map +1 -0
  44. package/dist/gc.d.ts +470 -0
  45. package/dist/gc.d.ts.map +1 -0
  46. package/dist/gc.js +819 -0
  47. package/dist/gc.js.map +1 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +42 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/touchAround.d.ts +46 -0
  53. package/dist/touchAround.d.ts.map +1 -0
  54. package/dist/touchAround.js +98 -0
  55. package/dist/touchAround.js.map +1 -0
  56. package/package.json +58 -3
  57. package/src/CachingContentStore.ts +184 -0
  58. package/src/ContentResolvingEventStore.ts +325 -0
  59. package/src/ContentStore.ts +317 -0
  60. package/src/ContentTouchingSnapshotStore.ts +109 -0
  61. package/src/citations.ts +117 -0
  62. package/src/content.bench.ts +84 -0
  63. package/src/content.ts +191 -0
  64. package/src/derive.ts +40 -0
  65. package/src/errors.ts +19 -0
  66. package/src/executionEnd.ts +64 -0
  67. package/src/gc.ts +1213 -0
  68. package/src/index.ts +54 -0
  69. package/src/touchAround.ts +131 -0
@@ -0,0 +1,56 @@
1
+ import type { Snapshot, SnapshotStore } from "@loomwork/snapshot-runtime";
2
+ import type { ContentStore } from "./ContentStore";
3
+ import { type TouchErrorHook } from "./touchAround";
4
+ /**
5
+ * The brand a {@link ContentTouchingSnapshotStore} carries (a registered symbol, so a second copy of
6
+ * this package still recognises it). Read it through {@link isContentTouchingSnapshotStore}.
7
+ */
8
+ export declare const CONTENT_TOUCHING_SNAPSHOT_STORE: unique symbol;
9
+ /**
10
+ * True when `store` is a {@link ContentTouchingSnapshotStore}. Evidence about THIS object only: a
11
+ * `false` does not prove the runtime wrote unwrapped (a sweep or the CLI reads a raw store over the
12
+ * same tables a wrapped one wrote), and a `true` does not prove every OTHER store the runtime saves
13
+ * to is wrapped. `sweepUnpinnedContent` therefore uses it only to warn, never to relax or refuse.
14
+ */
15
+ export declare function isContentTouchingSnapshotStore(store: unknown): boolean;
16
+ export interface ContentTouchingSnapshotStoreOptions {
17
+ /** See {@link TouchErrorHook}: the touch after a durable save, and a cited blob found missing. */
18
+ onTouchError?: TouchErrorHook;
19
+ }
20
+ /**
21
+ * ADR 0055 D6 amendment 2 — the SECOND citation path. `copyPrefix` copies only the post-gap events
22
+ * of a compacted parent; the refs its truncated prefix cited live in the child's SEED SNAPSHOT
23
+ * (the runtime's `fork` saves it), where an R5 media source or a claim-check envelope stays
24
+ * verbatim and nothing `put`s it again. `ContentResolvingEventStore` cannot see it, so this
25
+ * decorator touches the citations of every snapshot it SAVES, before and after, exactly as that
26
+ * store does for events (same walker, same guarantee: D5's margin, see {@link touchAround}).
27
+ *
28
+ * Chosen over a hook in the runtime because the runtime holds no content store (and must not
29
+ * depend on the content plugin), and over touching at fork start because a decorator also covers
30
+ * compaction checkpoints and every other save with no extra seam. Cost: one iterative walk of the
31
+ * saved state and a round trip only when it cites a blob; with a content store that has no `touch`
32
+ * nothing at all.
33
+ *
34
+ * Wrap it OUTSIDE an `EncryptingSnapshotStore` (it must see the plaintext state, or refs under a
35
+ * sensitive path are hidden from the walk), and pass the same content store the event log uses.
36
+ * The seed is saved after the prefix copy, so a fork racing the DELETION of its own parent is
37
+ * outside the guarantee (the fork's read of the parent fails then, too).
38
+ */
39
+ export declare class ContentTouchingSnapshotStore implements SnapshotStore {
40
+ private readonly inner;
41
+ private readonly content;
42
+ readonly deleteExecution?: NonNullable<SnapshotStore["deleteExecution"]>;
43
+ readonly loadLatestSealed?: NonNullable<SnapshotStore["loadLatestSealed"]>;
44
+ readonly pruneSnapshots?: NonNullable<SnapshotStore["pruneSnapshots"]>;
45
+ readonly listSequences?: NonNullable<SnapshotStore["listSequences"]>;
46
+ /** Brand: see {@link isContentTouchingSnapshotStore}. */
47
+ readonly [CONTENT_TOUCHING_SNAPSHOT_STORE] = true;
48
+ private readonly onTouchError;
49
+ constructor(inner: SnapshotStore, content: Pick<ContentStore, "touch">, options?: ContentTouchingSnapshotStoreOptions);
50
+ save(snapshot: Snapshot): Promise<void>;
51
+ loadLatest(executionId: string): Promise<Snapshot | null>;
52
+ loadAt(executionId: string, sequence: number): Promise<Snapshot | null>;
53
+ loadLatestBefore(executionId: string, sequence: number): Promise<Snapshot | null>;
54
+ close(): Promise<void>;
55
+ }
56
+ //# sourceMappingURL=ContentTouchingSnapshotStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContentTouchingSnapshotStore.d.ts","sourceRoot":"","sources":["../src/ContentTouchingSnapshotStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,QAAQ,EACR,aAAa,EACd,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,eAAe,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,OAAO,MAEpD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMtE;AAED,MAAM,WAAW,mCAAmC;IAClD,kGAAkG;IAClG,YAAY,CAAC,EAAE,cAAc,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,4BAA6B,YAAW,aAAa;IAU9D,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV1B,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvE,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;IACrE,yDAAyD;IACzD,QAAQ,CAAC,CAAC,+BAA+B,CAAC,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;gBAGvC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,EACrD,OAAO,GAAE,mCAAwC;IAgBnD,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIzD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIvE,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIjF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAGvB"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContentTouchingSnapshotStore = exports.CONTENT_TOUCHING_SNAPSHOT_STORE = void 0;
4
+ exports.isContentTouchingSnapshotStore = isContentTouchingSnapshotStore;
5
+ const event_runtime_1 = require("@loomwork/event-runtime");
6
+ const touchAround_1 = require("./touchAround");
7
+ /**
8
+ * The brand a {@link ContentTouchingSnapshotStore} carries (a registered symbol, so a second copy of
9
+ * this package still recognises it). Read it through {@link isContentTouchingSnapshotStore}.
10
+ */
11
+ exports.CONTENT_TOUCHING_SNAPSHOT_STORE = Symbol.for("loom.plugin-content.ContentTouchingSnapshotStore");
12
+ /**
13
+ * True when `store` is a {@link ContentTouchingSnapshotStore}. Evidence about THIS object only: a
14
+ * `false` does not prove the runtime wrote unwrapped (a sweep or the CLI reads a raw store over the
15
+ * same tables a wrapped one wrote), and a `true` does not prove every OTHER store the runtime saves
16
+ * to is wrapped. `sweepUnpinnedContent` therefore uses it only to warn, never to relax or refuse.
17
+ */
18
+ function isContentTouchingSnapshotStore(store) {
19
+ return (typeof store === "object" &&
20
+ store !== null &&
21
+ store[exports.CONTENT_TOUCHING_SNAPSHOT_STORE] === true);
22
+ }
23
+ /**
24
+ * ADR 0055 D6 amendment 2 — the SECOND citation path. `copyPrefix` copies only the post-gap events
25
+ * of a compacted parent; the refs its truncated prefix cited live in the child's SEED SNAPSHOT
26
+ * (the runtime's `fork` saves it), where an R5 media source or a claim-check envelope stays
27
+ * verbatim and nothing `put`s it again. `ContentResolvingEventStore` cannot see it, so this
28
+ * decorator touches the citations of every snapshot it SAVES, before and after, exactly as that
29
+ * store does for events (same walker, same guarantee: D5's margin, see {@link touchAround}).
30
+ *
31
+ * Chosen over a hook in the runtime because the runtime holds no content store (and must not
32
+ * depend on the content plugin), and over touching at fork start because a decorator also covers
33
+ * compaction checkpoints and every other save with no extra seam. Cost: one iterative walk of the
34
+ * saved state and a round trip only when it cites a blob; with a content store that has no `touch`
35
+ * nothing at all.
36
+ *
37
+ * Wrap it OUTSIDE an `EncryptingSnapshotStore` (it must see the plaintext state, or refs under a
38
+ * sensitive path are hidden from the walk), and pass the same content store the event log uses.
39
+ * The seed is saved after the prefix copy, so a fork racing the DELETION of its own parent is
40
+ * outside the guarantee (the fork's read of the parent fails then, too).
41
+ */
42
+ class ContentTouchingSnapshotStore {
43
+ inner;
44
+ content;
45
+ deleteExecution;
46
+ loadLatestSealed;
47
+ pruneSnapshots;
48
+ listSequences;
49
+ /** Brand: see {@link isContentTouchingSnapshotStore}. */
50
+ [exports.CONTENT_TOUCHING_SNAPSHOT_STORE] = true;
51
+ onTouchError;
52
+ constructor(inner, content, options = {}) {
53
+ this.inner = inner;
54
+ this.content = content;
55
+ this.onTouchError = options.onTouchError;
56
+ const plan = {
57
+ deleteExecution: "forward",
58
+ loadLatestSealed: "forward",
59
+ // ADR 0134 D6 — a delete cites nothing, so there is nothing to touch. A blob only a pruned
60
+ // snapshot cited was never pinned by the sweep (it pins the latest and the checkpoint,
61
+ // both of which pruning keeps), so no sweep decision changes.
62
+ pruneSnapshots: "forward",
63
+ // ADR 0134 amendment - a read of sequences cites no content.
64
+ listSequences: "forward",
65
+ };
66
+ (0, event_runtime_1.installOptional)(this, inner, plan);
67
+ }
68
+ save(snapshot) {
69
+ return (0, touchAround_1.touchAround)(this.content, [snapshot.state], { operation: "snapshotSave", executionId: snapshot.executionId }, this.onTouchError, () => this.inner.save(snapshot));
70
+ }
71
+ loadLatest(executionId) {
72
+ return this.inner.loadLatest(executionId);
73
+ }
74
+ loadAt(executionId, sequence) {
75
+ return this.inner.loadAt(executionId, sequence);
76
+ }
77
+ loadLatestBefore(executionId, sequence) {
78
+ return this.inner.loadLatestBefore(executionId, sequence);
79
+ }
80
+ close() {
81
+ return this.inner.close();
82
+ }
83
+ }
84
+ exports.ContentTouchingSnapshotStore = ContentTouchingSnapshotStore;
85
+ //# sourceMappingURL=ContentTouchingSnapshotStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContentTouchingSnapshotStore.js","sourceRoot":"","sources":["../src/ContentTouchingSnapshotStore.ts"],"names":[],"mappings":";;;AAuBA,wEAMC;AA7BD,2DAA8E;AAO9E,+CAAiE;AAEjE;;;GAGG;AACU,QAAA,+BAA+B,GAAkB,MAAM,CAAC,GAAG,CACtE,kDAAkD,CACnD,CAAC;AAEF;;;;;GAKG;AACH,SAAgB,8BAA8B,CAAC,KAAc;IAC3D,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAiC,CAAC,uCAA+B,CAAC,KAAK,IAAI,CAC7E,CAAC;AACJ,CAAC;AAOD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,4BAA4B;IAUpB;IACA;IAVV,eAAe,CAAiD;IAChE,gBAAgB,CAAkD;IAClE,cAAc,CAAgD;IAC9D,aAAa,CAA+C;IACrE,yDAAyD;IAChD,CAAC,uCAA+B,CAAC,GAAG,IAAI,CAAC;IACjC,YAAY,CAA6B;IAE1D,YACmB,KAAoB,EACpB,OAAoC,EACrD,UAA+C,EAAE;QAFhC,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAA6B;QAGrD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,MAAM,IAAI,GAAG;YACX,eAAe,EAAE,SAAS;YAC1B,gBAAgB,EAAE,SAAS;YAC3B,2FAA2F;YAC3F,uFAAuF;YACvF,8DAA8D;YAC9D,cAAc,EAAE,SAAS;YACzB,6DAA6D;YAC7D,aAAa,EAAE,SAAS;SAC2C,CAAC;QACtE,IAAA,+BAAe,EAA6C,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IACjF,CAAC;IAED,IAAI,CAAC,QAAkB;QACrB,OAAO,IAAA,yBAAW,EAChB,IAAI,CAAC,OAAO,EACZ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAChB,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,EAChE,IAAI,CAAC,YAAY,EACjB,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAChC,CAAC;IACJ,CAAC;IAED,UAAU,CAAC,WAAmB;QAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,CAAC,WAAmB,EAAE,QAAgB;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,gBAAgB,CAAC,WAAmB,EAAE,QAAgB;QACpD,OAAO,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF;AArDD,oEAqDC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Visit every content address an event payload (or snapshot state) cites. Any object carrying a
3
+ * string `__loomContent` is a ref leaf — this covers bare ADR 0046
4
+ * `ContentRef`s (e.g. a finding's `evidence` list, an externalized field) AND
5
+ * the `ref` of an ADR 0054 claim-check envelope. `inEnvelope` is true ONLY for
6
+ * a RECLAIMABLE envelope's own `ref` — the offloaded value, the one citation whose
7
+ * erasure is replay-invisible (ADR 0055 D4). An envelope marked `retain: true` (ADR 0140:
8
+ * the opt-in agent claim-check, whose offloaded bytes are the ONLY copy of a tool output the
9
+ * log once held) reports its ref as load-bearing (`inEnvelope` false): kept while any live
10
+ * log, archive segment or snapshot cites it, freed only when that history is deleted. Every other envelope field (`digest` above
11
+ * all: orchestration branches on it) is recorded, replay-visible data and is
12
+ * walked in the envelope's own context like any other payload.
13
+ *
14
+ * Because the enveloped context never descends (the `ref` is a leaf), one
15
+ * WeakSet cycle guard suffices: an object aliased at a bare site and as an
16
+ * envelope's `ref` is still visited bare, since the envelope reports its ref
17
+ * directly rather than through the guard.
18
+ *
19
+ * Iterative with cycle detection: the sweep is a retention job over
20
+ * app-controlled payloads, so a pathologically deep (or, from an in-memory
21
+ * store, cyclic) value must not stack-overflow the pass (audit W8).
22
+ */
23
+ export declare function visitCitedAddresses(value: unknown, visit: (address: string, inEnvelope: boolean) => void,
24
+ /**
25
+ * ADR 0140 D11 - when given, a `retain: true` envelope is honoured as load-bearing ONLY if it is in
26
+ * this set (see {@link retainRootsOf}); any other (a nested, tool-authored lookalike) stays reclaimable.
27
+ * Absent: every retained envelope is honoured (snapshots and archives, which pin everything anyway).
28
+ */
29
+ trustedRetain?: ReadonlySet<object>): void;
30
+ /**
31
+ * ADR 0140 D11 - the objects in an event payload where a retained envelope is TRUSTED: the top-level
32
+ * `output` of a `STEP_COMPLETED` (through a taint wrapper), exactly where the opt-in preset records its
33
+ * envelope. A `retain: true` lookalike nested inside a tool's result is tool-authored data: it cannot
34
+ * pin a blob.
35
+ */
36
+ export declare function retainRootsOf(event: {
37
+ eventType: string;
38
+ payload: unknown;
39
+ }): Set<object>;
40
+ //# sourceMappingURL=citations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citations.d.ts","sourceRoot":"","sources":["../src/citations.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,KAAK,IAAI;AACrD;;;;GAIG;AACH,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAClC,IAAI,CAyCN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAazF"}
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ // The ONE walker for "which blobs does this value cite" — shared by the mark phase of
3
+ // `sweepUnpinnedContent` and by `ContentResolvingEventStore`, which touches what it carries
4
+ // into a log without a `put` (ADR 0055 D6 amendment 2). Iterative + cycle-safe: neither a
5
+ // retention job nor an append may stack-overflow on an app-controlled payload.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.visitCitedAddresses = visitCitedAddresses;
8
+ exports.retainRootsOf = retainRootsOf;
9
+ /**
10
+ * The structural claim-check test, identical to `isClaimCheck` in `@loomwork/middleware`
11
+ * (kept structural so this plugin takes no middleware dependency): the flag AND a string
12
+ * `ref.__loomContent`. An app object that merely carries the flag is app data.
13
+ */
14
+ function envelopeRef(value) {
15
+ if (value.__loomClaimCheck !== true)
16
+ return undefined;
17
+ const address = value.ref?.__loomContent;
18
+ return typeof address === "string" ? address : undefined;
19
+ }
20
+ /** The address of an R5 media source `{ kind: "ref", ref: { address: string } }`, if `value` is one. */
21
+ function mediaSourceAddress(value) {
22
+ if (value.kind !== "ref")
23
+ return undefined;
24
+ const address = value.ref?.address;
25
+ return typeof address === "string" ? address : undefined;
26
+ }
27
+ /**
28
+ * Visit every content address an event payload (or snapshot state) cites. Any object carrying a
29
+ * string `__loomContent` is a ref leaf — this covers bare ADR 0046
30
+ * `ContentRef`s (e.g. a finding's `evidence` list, an externalized field) AND
31
+ * the `ref` of an ADR 0054 claim-check envelope. `inEnvelope` is true ONLY for
32
+ * a RECLAIMABLE envelope's own `ref` — the offloaded value, the one citation whose
33
+ * erasure is replay-invisible (ADR 0055 D4). An envelope marked `retain: true` (ADR 0140:
34
+ * the opt-in agent claim-check, whose offloaded bytes are the ONLY copy of a tool output the
35
+ * log once held) reports its ref as load-bearing (`inEnvelope` false): kept while any live
36
+ * log, archive segment or snapshot cites it, freed only when that history is deleted. Every other envelope field (`digest` above
37
+ * all: orchestration branches on it) is recorded, replay-visible data and is
38
+ * walked in the envelope's own context like any other payload.
39
+ *
40
+ * Because the enveloped context never descends (the `ref` is a leaf), one
41
+ * WeakSet cycle guard suffices: an object aliased at a bare site and as an
42
+ * envelope's `ref` is still visited bare, since the envelope reports its ref
43
+ * directly rather than through the guard.
44
+ *
45
+ * Iterative with cycle detection: the sweep is a retention job over
46
+ * app-controlled payloads, so a pathologically deep (or, from an in-memory
47
+ * store, cyclic) value must not stack-overflow the pass (audit W8).
48
+ */
49
+ function visitCitedAddresses(value, visit,
50
+ /**
51
+ * ADR 0140 D11 - when given, a `retain: true` envelope is honoured as load-bearing ONLY if it is in
52
+ * this set (see {@link retainRootsOf}); any other (a nested, tool-authored lookalike) stays reclaimable.
53
+ * Absent: every retained envelope is honoured (snapshots and archives, which pin everything anyway).
54
+ */
55
+ trustedRetain) {
56
+ const stack = [value];
57
+ const seen = new WeakSet();
58
+ while (stack.length > 0) {
59
+ const current = stack.pop();
60
+ if (typeof current !== "object" || current === null)
61
+ continue;
62
+ if (seen.has(current))
63
+ continue;
64
+ seen.add(current);
65
+ const address = current.__loomContent;
66
+ if (typeof address === "string") {
67
+ visit(address, false);
68
+ continue;
69
+ }
70
+ if (Array.isArray(current)) {
71
+ for (const item of current)
72
+ stack.push(item);
73
+ continue;
74
+ }
75
+ // R5 (ADR 0120) — an LLM media source `{ kind: "ref", ref: { address } }` cites a blob by its
76
+ // address WITHOUT the `__loomContent` marker (deliberately: the resolving store would
77
+ // JSON-decode that marker). It is as load-bearing as a bare ref — a live model call resolves
78
+ // it, and erasing it makes the next call fail — so the mark phase must see it, or the sweep
79
+ // would reclaim media the transcript still cites.
80
+ const mediaAddress = mediaSourceAddress(current);
81
+ if (mediaAddress !== undefined) {
82
+ visit(mediaAddress, false);
83
+ continue;
84
+ }
85
+ const offloaded = envelopeRef(current);
86
+ // ADR 0140 - a RETAINED envelope's blob is audit evidence, not telemetry: load-bearing.
87
+ if (offloaded !== undefined) {
88
+ visit(offloaded, current.retain !== true ||
89
+ (trustedRetain !== undefined && !trustedRetain.has(current)));
90
+ }
91
+ for (const [key, item] of Object.entries(current)) {
92
+ if (offloaded !== undefined && key === "ref")
93
+ continue;
94
+ stack.push(item);
95
+ }
96
+ }
97
+ }
98
+ /**
99
+ * ADR 0140 D11 - the objects in an event payload where a retained envelope is TRUSTED: the top-level
100
+ * `output` of a `STEP_COMPLETED` (through a taint wrapper), exactly where the opt-in preset records its
101
+ * envelope. A `retain: true` lookalike nested inside a tool's result is tool-authored data: it cannot
102
+ * pin a blob.
103
+ */
104
+ function retainRootsOf(event) {
105
+ const roots = new Set();
106
+ if (event.eventType !== "STEP_COMPLETED")
107
+ return roots;
108
+ let out = event.payload?.output;
109
+ if (typeof out === "object" &&
110
+ out !== null &&
111
+ out.__loomTainted !== undefined) {
112
+ out = out.value;
113
+ }
114
+ if (typeof out === "object" && out !== null)
115
+ roots.add(out);
116
+ return roots;
117
+ }
118
+ //# sourceMappingURL=citations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citations.js","sourceRoot":"","sources":["../src/citations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=content.bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.bench.d.ts","sourceRoot":"","sources":["../src/content.bench.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const event_runtime_1 = require("@loomwork/event-runtime");
4
+ const vitest_1 = require("vitest");
5
+ const ContentResolvingEventStore_1 = require("./ContentResolvingEventStore");
6
+ const ContentStore_1 = require("./ContentStore");
7
+ // ── ADR 0062 guardrail bench — frozen-snapshot serving (TS-10) ────────────────
8
+ //
9
+ // 50 externalized refs (one dashboard's widgets) read through a backend with
10
+ // ~2ms simulated round-trip latency. Compare: batched getMany (one round-trip)
11
+ // vs the no-getMany fallback (coalesced parallel gets) vs the pre-ADR-0062
12
+ // baseline shape (serialized per-ref awaits). Budget: p95 ≤ 150ms for ≤ 50 refs
13
+ // — the batched path should sit near the single round-trip floor.
14
+ const LATENCY_MS = 2;
15
+ const WIDGETS = 50;
16
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
+ function withLatency(inner, supportsGetMany) {
18
+ const store = {
19
+ put: (bytes) => inner.put(bytes),
20
+ get: async (ref) => {
21
+ await sleep(LATENCY_MS);
22
+ return inner.get(ref);
23
+ },
24
+ delete: (ref) => inner.delete(ref),
25
+ };
26
+ if (supportsGetMany) {
27
+ store.getMany = async (refs) => {
28
+ await sleep(LATENCY_MS);
29
+ return inner.getMany(refs);
30
+ };
31
+ }
32
+ return store;
33
+ }
34
+ function envelope(sequence, payload) {
35
+ return {
36
+ id: `e-${sequence}`,
37
+ executionId: "x-1",
38
+ workflowId: "w-1",
39
+ eventType: "STEP_COMPLETED",
40
+ sequence,
41
+ timestamp: 1_000 + sequence,
42
+ payload,
43
+ };
44
+ }
45
+ async function seeded(supportsGetMany) {
46
+ const memory = new ContentStore_1.InMemoryContentStore();
47
+ const content = withLatency(memory, supportsGetMany);
48
+ const store = new ContentResolvingEventStore_1.ContentResolvingEventStore(new event_runtime_1.InMemoryAsyncEventStore(), content, {
49
+ externalize: { STEP_COMPLETED: ["output.doc"] },
50
+ });
51
+ for (let i = 0; i < WIDGETS; i++) {
52
+ await store.append(envelope(i, { stepId: `hydrate#${i}`, output: { doc: { widget: i } } }));
53
+ }
54
+ return store;
55
+ }
56
+ let batched;
57
+ let fallback;
58
+ (0, vitest_1.describe)(`resolve ${WIDGETS} refs @ ${LATENCY_MS}ms backend latency`, () => {
59
+ (0, vitest_1.bench)("getMany (one batched round-trip)", async () => {
60
+ batched ??= await seeded(true);
61
+ await batched.read("x-1");
62
+ });
63
+ (0, vitest_1.bench)("no getMany (coalesced parallel gets)", async () => {
64
+ fallback ??= await seeded(false);
65
+ await fallback.read("x-1");
66
+ });
67
+ (0, vitest_1.bench)("pre-ADR-0062 shape (serialized per-ref awaits — baseline)", async () => {
68
+ // The old resolver awaited object fields sequentially; emulate its
69
+ // round-trip profile directly against the latency store.
70
+ for (let i = 0; i < WIDGETS; i++) {
71
+ await sleep(LATENCY_MS);
72
+ }
73
+ });
74
+ });
75
+ //# sourceMappingURL=content.bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.bench.js","sourceRoot":"","sources":["../src/content.bench.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /** A content-addressed reference standing in for an externalized value in the log. */
2
+ export interface ContentRef {
3
+ /** The content address, e.g. `"sha256:<hex>"`. Uniquely and deterministically names the bytes. */
4
+ readonly __loomContent: string;
5
+ /** Byte length of the stored content (informational). */
6
+ readonly size?: number;
7
+ /** How the bytes decode back to a value (only `"json"` today). */
8
+ readonly codec?: string;
9
+ }
10
+ export declare function isContentRef(value: unknown): value is ContentRef;
11
+ /** The content address of some bytes: `sha256:<hex>`. Pure — the same bytes always map here. */
12
+ export declare function contentAddress(bytes: Uint8Array): string;
13
+ /** Encode a value to bytes for the content store (JSON — the only codec today). */
14
+ export declare function encodeValue(value: unknown): Uint8Array;
15
+ /** Decode content-store bytes back to the original value. */
16
+ export declare function decodeValue(bytes: Uint8Array): unknown;
17
+ /** Whether a payload carries any {@link ContentRef} (the read-side fast path). */
18
+ export declare function hasContentRef(value: unknown): boolean;
19
+ /**
20
+ * ADR 0062 — phase 1 of batched resolution: collect every {@link ContentRef} in
21
+ * a payload into `into` (keyed by content address — duplicates collapse), and
22
+ * report whether any was found. One walk replaces the old `hasContentRef`
23
+ * pre-walk + per-ref fetch recursion.
24
+ */
25
+ export declare function collectContentRefs(value: unknown, into: Map<string, ContentRef>): boolean;
26
+ /**
27
+ * ADR 0062 — phase 2 of batched resolution: pure rebuild substituting each
28
+ * {@link ContentRef} from the fetched bytes. Decoding is deliberately
29
+ * per-OCCURRENCE (the fetch dedupes by address — the expensive part — but two
30
+ * payload sites never alias one decoded object). Ref-free subtrees are returned
31
+ * as-is (structural sharing).
32
+ */
33
+ export declare function substituteContentRefs(value: unknown, bytesByAddress: ReadonlyMap<string, Uint8Array>): unknown;
34
+ /**
35
+ * Async structural path-mapper — like the crypto plugin's `mapPaths`, but the transform is
36
+ * awaited (a content `put` is async). Copies only the spine it touches (structural sharing
37
+ * elsewhere), never mutates `root`, and skips absent paths.
38
+ */
39
+ export declare function mapPathsAsync(root: unknown, paths: readonly string[], transform: (value: unknown) => Promise<unknown>): Promise<unknown>;
40
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAWA,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,kGAAkG;IAClG,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAExD;AAOD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAEtD;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEtD;AAuBD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQrD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,OAAO,CAmBzF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GAC9C,OAAO,CA6BT;AAQD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC9C,OAAO,CAAC,OAAO,CAAC,CA0BlB"}
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isContentRef = isContentRef;
4
+ exports.contentAddress = contentAddress;
5
+ exports.encodeValue = encodeValue;
6
+ exports.decodeValue = decodeValue;
7
+ exports.hasContentRef = hasContentRef;
8
+ exports.collectContentRefs = collectContentRefs;
9
+ exports.substituteContentRefs = substituteContentRefs;
10
+ exports.mapPathsAsync = mapPathsAsync;
11
+ const node_crypto_1 = require("node:crypto");
12
+ const classify_1 = require("@loomwork/classify");
13
+ function isContentRef(value) {
14
+ return (typeof value === "object" &&
15
+ value !== null &&
16
+ typeof value.__loomContent === "string");
17
+ }
18
+ /** The content address of some bytes: `sha256:<hex>`. Pure — the same bytes always map here. */
19
+ function contentAddress(bytes) {
20
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex")}`;
21
+ }
22
+ // Stateless for whole-input encode/decode — hoisted (audit nit: decodeValue runs
23
+ // per ref occurrence during substitution, incl. the live tail).
24
+ const UTF8_ENCODER = new TextEncoder();
25
+ const UTF8_DECODER = new TextDecoder();
26
+ /** Encode a value to bytes for the content store (JSON — the only codec today). */
27
+ function encodeValue(value) {
28
+ return UTF8_ENCODER.encode(JSON.stringify(value));
29
+ }
30
+ /** Decode content-store bytes back to the original value. */
31
+ function decodeValue(bytes) {
32
+ return JSON.parse(UTF8_DECODER.decode(bytes));
33
+ }
34
+ /**
35
+ * An ADR 0054 claim-check envelope, recognised structurally with the same test as
36
+ * `@loomwork/middleware`'s `isClaimCheck` (`__loomClaimCheck === true` AND a string
37
+ * `ref.__loomContent`), so this plugin takes no dependency for it (ADR 0055 D3).
38
+ *
39
+ * The read-side walkers below treat one as a LEAF. An envelope is resolved only by the
40
+ * claim-check plugin (`resolveClaimCheck`): its `ref` is a TTL-reclaimable citation whose
41
+ * erasure the fold never sees, and its bytes may not be JSON (`codec: "bytes"`). Descending
42
+ * into it replaced `ref` with the decoded value, threw `ContentMissingError` once the sweep
43
+ * had reclaimed the blob, and failed outright on a binary one (ADR 0055 D4's known gap).
44
+ */
45
+ function isClaimCheckEnvelope(value) {
46
+ const v = value;
47
+ return (v.__loomClaimCheck === true &&
48
+ typeof v.ref === "object" &&
49
+ v.ref !== null &&
50
+ typeof v.ref.__loomContent === "string");
51
+ }
52
+ /** Whether a payload carries any {@link ContentRef} (the read-side fast path). */
53
+ function hasContentRef(value) {
54
+ if (isContentRef(value))
55
+ return true;
56
+ if (Array.isArray(value))
57
+ return value.some(hasContentRef);
58
+ if (typeof value === "object" && value !== null) {
59
+ if (isClaimCheckEnvelope(value))
60
+ return false; // the claim-check plugin's to resolve
61
+ return Object.values(value).some(hasContentRef);
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * ADR 0062 — phase 1 of batched resolution: collect every {@link ContentRef} in
67
+ * a payload into `into` (keyed by content address — duplicates collapse), and
68
+ * report whether any was found. One walk replaces the old `hasContentRef`
69
+ * pre-walk + per-ref fetch recursion.
70
+ */
71
+ function collectContentRefs(value, into) {
72
+ if (isContentRef(value)) {
73
+ if (!into.has(value.__loomContent))
74
+ into.set(value.__loomContent, value);
75
+ return true;
76
+ }
77
+ if (Array.isArray(value)) {
78
+ let found = false;
79
+ for (const v of value)
80
+ if (collectContentRefs(v, into))
81
+ found = true;
82
+ return found;
83
+ }
84
+ if (typeof value === "object" && value !== null) {
85
+ if (isClaimCheckEnvelope(value))
86
+ return false; // a leaf: see isClaimCheckEnvelope
87
+ let found = false;
88
+ for (const v of Object.values(value)) {
89
+ if (collectContentRefs(v, into))
90
+ found = true;
91
+ }
92
+ return found;
93
+ }
94
+ return false;
95
+ }
96
+ /**
97
+ * ADR 0062 — phase 2 of batched resolution: pure rebuild substituting each
98
+ * {@link ContentRef} from the fetched bytes. Decoding is deliberately
99
+ * per-OCCURRENCE (the fetch dedupes by address — the expensive part — but two
100
+ * payload sites never alias one decoded object). Ref-free subtrees are returned
101
+ * as-is (structural sharing).
102
+ */
103
+ function substituteContentRefs(value, bytesByAddress) {
104
+ if (isContentRef(value)) {
105
+ const bytes = bytesByAddress.get(value.__loomContent);
106
+ // Callers guarantee every collected address was fetched; a miss here is a
107
+ // programming error, surfaced loudly.
108
+ if (bytes === undefined)
109
+ throw new Error(`unfetched content ref: ${value.__loomContent}`);
110
+ return decodeValue(bytes);
111
+ }
112
+ if (Array.isArray(value)) {
113
+ let changed = false;
114
+ const out = value.map((v) => {
115
+ const r = substituteContentRefs(v, bytesByAddress);
116
+ if (r !== v)
117
+ changed = true;
118
+ return r;
119
+ });
120
+ return changed ? out : value;
121
+ }
122
+ if (typeof value === "object" && value !== null) {
123
+ if (isClaimCheckEnvelope(value))
124
+ return value; // a leaf: see isClaimCheckEnvelope
125
+ let changed = false;
126
+ const out = {};
127
+ for (const [k, v] of Object.entries(value)) {
128
+ const r = substituteContentRefs(v, bytesByAddress);
129
+ out[k] = r;
130
+ if (r !== v)
131
+ changed = true;
132
+ }
133
+ return changed ? out : value;
134
+ }
135
+ return value;
136
+ }
137
+ function shallowCopy(value) {
138
+ return Array.isArray(value)
139
+ ? [...value]
140
+ : { ...value };
141
+ }
142
+ /**
143
+ * Async structural path-mapper — like the crypto plugin's `mapPaths`, but the transform is
144
+ * awaited (a content `put` is async). Copies only the spine it touches (structural sharing
145
+ * elsewhere), never mutates `root`, and skips absent paths.
146
+ */
147
+ async function mapPathsAsync(root, paths, transform) {
148
+ if (paths.length === 0 || typeof root !== "object" || root === null)
149
+ return root;
150
+ // `[]` segments are resolved against the concrete value first, so a mark inside an array
151
+ // element reaches every element (#260). Paths without one pass through untouched.
152
+ const targets = (0, classify_1.hasWildcard)(paths) ? (0, classify_1.expandPaths)(root, paths) : paths;
153
+ const out = shallowCopy(root);
154
+ for (const path of targets) {
155
+ const segs = path.split(".");
156
+ let src = root;
157
+ let dst = out;
158
+ let reachable = true;
159
+ for (let i = 0; i < segs.length - 1; i++) {
160
+ const seg = segs[i];
161
+ const child = src[seg];
162
+ if (typeof child !== "object" || child === null) {
163
+ reachable = false;
164
+ break;
165
+ }
166
+ if (dst[seg] === child)
167
+ dst[seg] = shallowCopy(child);
168
+ src = child;
169
+ dst = dst[seg];
170
+ }
171
+ const leaf = segs[segs.length - 1];
172
+ if (reachable && Object.hasOwn(src, leaf))
173
+ dst[leaf] = await transform(src[leaf]);
174
+ }
175
+ return out;
176
+ }
177
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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"}