@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.21

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 (195) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +76 -2
  3. package/cordis.patch.yml +363 -0
  4. package/docs/architecture.md +692 -0
  5. package/docs/autorun-guide.md +248 -0
  6. package/docs/backlog.md +2022 -0
  7. package/docs/contract.md +938 -0
  8. package/docs/development.md +990 -0
  9. package/docs/host-compaction-seam.md +225 -0
  10. package/docs/host-protected-head-ask.md +80 -0
  11. package/docs/knowledge-repo.md +1332 -0
  12. package/docs/plans-carriage.md +571 -0
  13. package/docs/project-state.md +642 -0
  14. package/docs/provider.md +98 -0
  15. package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
  16. package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
  17. package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
  18. package/docs/verify.md +561 -0
  19. package/lib/archive.d.ts +161 -0
  20. package/lib/archive.js +229 -0
  21. package/lib/archive.js.map +1 -0
  22. package/lib/arrival.d.ts +23 -0
  23. package/lib/arrival.js +125 -0
  24. package/lib/arrival.js.map +1 -0
  25. package/lib/artifact-query.d.ts +62 -0
  26. package/lib/artifact-query.js +187 -0
  27. package/lib/artifact-query.js.map +1 -0
  28. package/lib/attach.d.ts +46 -0
  29. package/lib/attach.js +120 -0
  30. package/lib/attach.js.map +1 -0
  31. package/lib/auto-save.d.ts +145 -0
  32. package/lib/auto-save.js +334 -0
  33. package/lib/auto-save.js.map +1 -0
  34. package/lib/aux-message.d.ts +17 -0
  35. package/lib/aux-message.js +20 -0
  36. package/lib/aux-message.js.map +1 -0
  37. package/lib/client.js +1419 -0
  38. package/lib/client.js.map +1 -0
  39. package/lib/codegraph.d.ts +76 -0
  40. package/lib/codegraph.js +190 -0
  41. package/lib/codegraph.js.map +1 -0
  42. package/lib/commands.d.ts +52 -0
  43. package/lib/commands.js +946 -0
  44. package/lib/commands.js.map +1 -0
  45. package/lib/compose.d.ts +30 -0
  46. package/lib/compose.js +185 -0
  47. package/lib/compose.js.map +1 -0
  48. package/lib/continue-core.d.ts +245 -0
  49. package/lib/continue-core.js +405 -0
  50. package/lib/continue-core.js.map +1 -0
  51. package/lib/diag.d.ts +55 -0
  52. package/lib/diag.js +186 -0
  53. package/lib/diag.js.map +1 -0
  54. package/lib/engine-core.d.ts +301 -0
  55. package/lib/engine-core.js +531 -0
  56. package/lib/engine-core.js.map +1 -0
  57. package/lib/engine-note.d.ts +35 -0
  58. package/lib/engine-note.js +32 -0
  59. package/lib/engine-note.js.map +1 -0
  60. package/lib/engine.d.ts +329 -0
  61. package/lib/engine.js +1179 -0
  62. package/lib/engine.js.map +1 -0
  63. package/lib/enrich-queue.d.ts +66 -0
  64. package/lib/enrich-queue.js +126 -0
  65. package/lib/enrich-queue.js.map +1 -0
  66. package/lib/enrich-store.d.ts +31 -0
  67. package/lib/enrich-store.js +103 -0
  68. package/lib/enrich-store.js.map +1 -0
  69. package/lib/enrich-wire.d.ts +163 -0
  70. package/lib/enrich-wire.js +353 -0
  71. package/lib/enrich-wire.js.map +1 -0
  72. package/lib/enrich.d.ts +102 -0
  73. package/lib/enrich.js +267 -0
  74. package/lib/enrich.js.map +1 -0
  75. package/lib/gitops.d.ts +107 -0
  76. package/lib/gitops.js +539 -0
  77. package/lib/gitops.js.map +1 -0
  78. package/lib/index.d.ts +241 -0
  79. package/lib/index.js +1162 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/indexing.d.ts +98 -0
  82. package/lib/indexing.js +223 -0
  83. package/lib/indexing.js.map +1 -0
  84. package/lib/injections.d.ts +90 -0
  85. package/lib/injections.js +165 -0
  86. package/lib/injections.js.map +1 -0
  87. package/lib/last-resort.d.ts +24 -0
  88. package/lib/last-resort.js +27 -0
  89. package/lib/last-resort.js.map +1 -0
  90. package/lib/log-shape.d.ts +38 -0
  91. package/lib/log-shape.js +93 -0
  92. package/lib/log-shape.js.map +1 -0
  93. package/lib/models-command.d.ts +74 -0
  94. package/lib/models-command.js +100 -0
  95. package/lib/models-command.js.map +1 -0
  96. package/lib/notice.d.ts +74 -0
  97. package/lib/notice.js +92 -0
  98. package/lib/notice.js.map +1 -0
  99. package/lib/plans.d.ts +154 -0
  100. package/lib/plans.js +315 -0
  101. package/lib/plans.js.map +1 -0
  102. package/lib/preset.d.ts +186 -0
  103. package/lib/preset.js +212 -0
  104. package/lib/preset.js.map +1 -0
  105. package/lib/prompt-keys.d.ts +7 -0
  106. package/lib/prompt-keys.js +8 -0
  107. package/lib/prompt-keys.js.map +1 -0
  108. package/lib/prompt-loop.d.ts +73 -0
  109. package/lib/prompt-loop.js +147 -0
  110. package/lib/prompt-loop.js.map +1 -0
  111. package/lib/prompt-push.d.ts +47 -0
  112. package/lib/prompt-push.js +115 -0
  113. package/lib/prompt-push.js.map +1 -0
  114. package/lib/prompt-runner.d.ts +48 -0
  115. package/lib/prompt-runner.js +90 -0
  116. package/lib/prompt-runner.js.map +1 -0
  117. package/lib/prompt-state.d.ts +142 -0
  118. package/lib/prompt-state.js +101 -0
  119. package/lib/prompt-state.js.map +1 -0
  120. package/lib/prompt.d.ts +270 -0
  121. package/lib/prompt.js +408 -0
  122. package/lib/prompt.js.map +1 -0
  123. package/lib/provider.d.ts +44 -0
  124. package/lib/provider.js +66 -0
  125. package/lib/provider.js.map +1 -0
  126. package/lib/redact.d.ts +24 -0
  127. package/lib/redact.js +85 -0
  128. package/lib/redact.js.map +1 -0
  129. package/lib/registry.d.ts +187 -0
  130. package/lib/registry.js +235 -0
  131. package/lib/registry.js.map +1 -0
  132. package/lib/removal.d.ts +159 -0
  133. package/lib/removal.js +556 -0
  134. package/lib/removal.js.map +1 -0
  135. package/lib/render.d.ts +97 -0
  136. package/lib/render.js +530 -0
  137. package/lib/render.js.map +1 -0
  138. package/lib/repair-run.d.ts +76 -0
  139. package/lib/repair-run.js +506 -0
  140. package/lib/repair-run.js.map +1 -0
  141. package/lib/repair.d.ts +208 -0
  142. package/lib/repair.js +357 -0
  143. package/lib/repair.js.map +1 -0
  144. package/lib/repo.d.ts +78 -0
  145. package/lib/repo.js +184 -0
  146. package/lib/repo.js.map +1 -0
  147. package/lib/rules-commands.d.ts +46 -0
  148. package/lib/rules-commands.js +379 -0
  149. package/lib/rules-commands.js.map +1 -0
  150. package/lib/rules.d.ts +267 -0
  151. package/lib/rules.js +478 -0
  152. package/lib/rules.js.map +1 -0
  153. package/lib/save-core.d.ts +72 -0
  154. package/lib/save-core.js +220 -0
  155. package/lib/save-core.js.map +1 -0
  156. package/lib/search.d.ts +112 -0
  157. package/lib/search.js +282 -0
  158. package/lib/search.js.map +1 -0
  159. package/lib/session-rules.d.ts +222 -0
  160. package/lib/session-rules.js +454 -0
  161. package/lib/session-rules.js.map +1 -0
  162. package/lib/signature.d.ts +45 -0
  163. package/lib/signature.js +199 -0
  164. package/lib/signature.js.map +1 -0
  165. package/lib/store.d.ts +156 -0
  166. package/lib/store.js +368 -0
  167. package/lib/store.js.map +1 -0
  168. package/lib/sync.d.ts +220 -0
  169. package/lib/sync.js +810 -0
  170. package/lib/sync.js.map +1 -0
  171. package/lib/tools.d.ts +113 -0
  172. package/lib/tools.js +1090 -0
  173. package/lib/tools.js.map +1 -0
  174. package/lib/treedx/client.d.ts +57 -0
  175. package/lib/treedx/client.js +82 -0
  176. package/lib/treedx/client.js.map +1 -0
  177. package/lib/treedx/provider.d.ts +39 -0
  178. package/lib/treedx/provider.js +479 -0
  179. package/lib/treedx/provider.js.map +1 -0
  180. package/lib/treedx/state.d.ts +27 -0
  181. package/lib/treedx/state.js +65 -0
  182. package/lib/treedx/state.js.map +1 -0
  183. package/lib/types.d.ts +210 -0
  184. package/lib/types.js +31 -0
  185. package/lib/types.js.map +1 -0
  186. package/lib/version.d.ts +9 -0
  187. package/lib/version.js +10 -0
  188. package/lib/version.js.map +1 -0
  189. package/lib/vocabulary.d.ts +41 -0
  190. package/lib/vocabulary.js +118 -0
  191. package/lib/vocabulary.js.map +1 -0
  192. package/lib/warnings.d.ts +10 -0
  193. package/lib/warnings.js +18 -0
  194. package/lib/warnings.js.map +1 -0
  195. package/package.json +151 -3
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Archive writer — the first I/O layer, behind an injected fs so it stays testable without a harness.
3
+ *
4
+ * Ordering is the whole design here. Reserve numbers → write artifacts → write chapters → verify read-back
5
+ * → return records for the registry. A crash must be able to leave an orphan file, but never a registry
6
+ * entry or an index that points at a file which was not written. See docs/architecture.md § Atomicity.
7
+ *
8
+ * Deferral is a copy decision, not a data decision: the artifact is written whether or not the chapter body
9
+ * also inlines it, so a wrong model judgement costs one `read` and never loses text.
10
+ */
11
+ import type { RenderedChapter } from './types.ts';
12
+ /** Minimal filesystem seam. `ctx.fs` is OPTIONAL in DSH, so absence must be a clean refusal upstream. */
13
+ export interface ArchiveFs {
14
+ write(path: string, content: string): Promise<void>;
15
+ read(path: string): Promise<string | undefined>;
16
+ exists(path: string): Promise<boolean>;
17
+ }
18
+ /** Number allocation through the registry, never by scanning a directory (races a concurrent sibling). */
19
+ export interface NumberAllocator {
20
+ /** Free an attempt key when NOTHING landed, so a failed door cannot leave keys accumulating. */
21
+ release?(attemptId: string): Promise<void>;
22
+ reserve(key: string, count: number): Promise<number[]>;
23
+ /**
24
+ * Separate allocation for plan DOCUMENTS (docs/plans-carriage.md § 11), so a plan
25
+ * never consumes a chapter number or reorders the chapter list. Optional: a writer
26
+ * without it simply emits no documents, and the chapters still cite their plans.
27
+ */
28
+ reservePlan?(key: string, count: number): Promise<number[]>;
29
+ }
30
+ export interface ChapterRecord {
31
+ number: number;
32
+ path: string;
33
+ title: string;
34
+ summary: string;
35
+ startSeq: number;
36
+ endSeq: number;
37
+ /** Index topics (deterministic floor; P2 enrichment may relabel). */
38
+ topics: string[];
39
+ /** user+assistant message count in the chapter (record §7.3 notice line);
40
+ * absent for records written before counts existed — never a lying zero.
41
+ * (`| undefined` for the `exactOptionalPropertyTypes` zod round-trip.) */
42
+ messages?: number | undefined;
43
+ /**
44
+ * The exact surface seqs archived, in surface order. Optional because
45
+ * continuation-path ranges are numeric-contiguous (start/end suffice), but
46
+ * engine-path spans replace across the log, where start/end are only span
47
+ * bounds — the set is the truth about coverage. (`| undefined` so the
48
+ * zod-inferred type satisfies `exactOptionalPropertyTypes` in store.ts.)
49
+ */
50
+ shadowedSeqs?: number[] | undefined;
51
+ /** Body sha256, used to detect on-disk tampering before the text is ever cited. */
52
+ sha256: string;
53
+ estimatedTokens: number;
54
+ artifacts: Array<{
55
+ path: string;
56
+ sha256: string;
57
+ bytes: number;
58
+ }>;
59
+ /**
60
+ * Plans this chapter carries (docs/plans-carriage.md § 2/§ 8): the durable,
61
+ * machine-readable half of the Plans block. Absent on records written before
62
+ * the feature — the notice reads `undefined` as "no plans here", never as a
63
+ * lie. The full bytes live in the cited artifact; the prose lives in the file.
64
+ */
65
+ plans?: import('./types.ts').PlanFact[] | undefined;
66
+ /** Folded plan-mode state at the chapter's end ('unknown' when the log never said). */
67
+ planMode?: 'active' | 'inactive' | 'unknown' | undefined;
68
+ /** Where each plan's own `kind: plan` document landed (docs/plans-carriage.md § 11). */
69
+ planDocs?: Array<{
70
+ seq: number;
71
+ path: string;
72
+ sha256: string;
73
+ }> | undefined;
74
+ /**
75
+ * Set by repair (#60): this record was superseded by chapter `supersededBy`. Set by operator
76
+ * removal (#255): the chapter's bytes are gone from the store and the mirror, and only this
77
+ * stub remains — the record, its number and its seqs stay in the registry forever, because
78
+ * dropping a record would rewind the watermark (`max(endSeq)+1`) and let a spent number be
79
+ * re-spent (#59). A tombstoned chapter is NOT knowledge — no TOC bullet, no enrichment pass,
80
+ * no search entry — under EITHER status, through the ONE predicate `isActiveChapter`.
81
+ */
82
+ status?: 'superseded' | 'removed' | undefined;
83
+ supersededBy?: number | undefined;
84
+ /** ISO instant the operator removed the chapter (#255). Absent on every other record. */
85
+ removedAt?: string | undefined;
86
+ /** Why this chapter exists as a replacement, and which number it replaced (`{class, over, at}`). */
87
+ repairs?: {
88
+ class: string;
89
+ over: number;
90
+ at: number;
91
+ }[] | undefined;
92
+ }
93
+ export interface WriteResult {
94
+ records: ChapterRecord[];
95
+ artifactPathsWritten: string[];
96
+ /** Duplicate titles, retried attempts reusing numbers, or content that failed read-back. */
97
+ warnings: string[];
98
+ }
99
+ /** Filesystem-safe, stable, and never empty. Title is display metadata; the number is the identity. */
100
+ export declare function slugify(title: string): string;
101
+ /**
102
+ * The ONE canonical form of a stored path (#69): **workspace-relative**. `ChapterRecord.path` had two
103
+ * producers — the doors mint from a relative `artifactStoreRoot`, the compaction realm from a resolved
104
+ * ABSOLUTE one — so one file could be a cited record by one path and an "unregistered file" by the
105
+ * other, and `/chapters-repair` then reported its own archive as lost knowledge (measured: 6 records,
106
+ * chapter 1 of the two longest sessions, each `msgs > 300`, all absolute).
107
+ *
108
+ * Relative is the right winner: the store root lives inside the workspace, `read` reaches it, and a
109
+ * path that survives moving a checkout between machines is the same argument `projectKey` already makes
110
+ * for the remote over the path. An absolute path OUTSIDE the workspace is passed through unchanged — the
111
+ * rig legitimately pins an absolute store root per port (#61 item 8), and pretending otherwise would
112
+ * produce a `../../..` path that resolves nowhere.
113
+ */
114
+ export declare function toStoredPath(cwd: string | undefined, p: string): string;
115
+ /**
116
+ * The READ direction of the stored-path dialect: turn a `ChapterRecord.path` back into something the
117
+ * filesystem understands. `toStoredPath` may hand back an ABSOLUTE path (the rig pins an absolute
118
+ * `artifactStoreRoot` per port — #61 item 8), and `path.join(cwd, absolute)` CONCATENATES instead of
119
+ * resolving, so a naive join sends a reader to `<cwd>/tmp/…`, which exists nowhere. Measured on the
120
+ * `enrich` leg 2026-10-24: the ladder reported `chapter file absent` about a file that was present,
121
+ * because of exactly this join.
122
+ */
123
+ export declare function resolveStoredPath(cwd: string | undefined, stored: string): string;
124
+ /** Compare two stored paths in either dialect. Every path COMPARISON goes through this, never `===`. */
125
+ export declare function sameStoredPath(a: string, b: string, cwd?: string): boolean;
126
+ export declare const chapterFileName: (n: number, title: string) => string;
127
+ /** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
128
+ /**
129
+ * The idempotence key for one archive attempt. `salt` exists for REPAIR (#60): a repair re-rendering a
130
+ * span the original door keyed would otherwise be swallowed as a retry of that attempt, and a swallowed
131
+ * repair is indistinguishable from one that worked.
132
+ */
133
+ export declare const attemptKey: (sourceSessionId: string, archiveCeiling: number, salt?: string) => string;
134
+ export interface WriteInput {
135
+ fs: ArchiveFs;
136
+ allocator: NumberAllocator;
137
+ /** Store root, relative to the session workspace so the `read` tool can reach it. */
138
+ storeRoot: string;
139
+ /** Workspace cwd; when given, stored paths are normalized to workspace-relative (#69). */
140
+ cwd?: string | undefined;
141
+ /** Directory key — the root of the ancestry tree, not the per-fork id. One dir per tree. */
142
+ rootSessionId: string;
143
+ chapters: readonly RenderedChapter[];
144
+ /** Reuse a prior attempt's allocation when this is a retry of identical work. */
145
+ attemptId: string;
146
+ }
147
+ export declare function writeArchive(input: WriteInput): Promise<WriteResult>;
148
+ /** Coverage arithmetic: which seqs below the ceiling were NOT archived. Gaps are reported, never silent. */
149
+ export declare function coverage(records: readonly {
150
+ startSeq: number;
151
+ endSeq: number;
152
+ }[], archiveCeiling: number, archiveFloor?: number): {
153
+ covered: number;
154
+ gaps: Array<{
155
+ from: number;
156
+ to: number;
157
+ }>;
158
+ };
159
+ export type VerifyResult = 'ok' | 'modified' | 'missing';
160
+ /** Called before citing a chapter in an index. Detects accidental drift and single-shot injection. */
161
+ export declare function verifyChapter(fs: ArchiveFs, record: ChapterRecord): Promise<VerifyResult>;
package/lib/archive.js ADDED
@@ -0,0 +1,229 @@
1
+ import { sha256 } from "./render.js";
2
+ /** Filesystem-safe, stable, and never empty. Title is display metadata; the number is the identity. */
3
+ export function slugify(title) {
4
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
5
+ return slug.length > 0 ? slug.slice(0, 48) : 'chapter';
6
+ }
7
+ /**
8
+ * The ONE canonical form of a stored path (#69): **workspace-relative**. `ChapterRecord.path` had two
9
+ * producers — the doors mint from a relative `artifactStoreRoot`, the compaction realm from a resolved
10
+ * ABSOLUTE one — so one file could be a cited record by one path and an "unregistered file" by the
11
+ * other, and `/chapters-repair` then reported its own archive as lost knowledge (measured: 6 records,
12
+ * chapter 1 of the two longest sessions, each `msgs > 300`, all absolute).
13
+ *
14
+ * Relative is the right winner: the store root lives inside the workspace, `read` reaches it, and a
15
+ * path that survives moving a checkout between machines is the same argument `projectKey` already makes
16
+ * for the remote over the path. An absolute path OUTSIDE the workspace is passed through unchanged — the
17
+ * rig legitimately pins an absolute store root per port (#61 item 8), and pretending otherwise would
18
+ * produce a `../../..` path that resolves nowhere.
19
+ */
20
+ export function toStoredPath(cwd, p) {
21
+ if (cwd === undefined || cwd === '' || !p.startsWith('/'))
22
+ return p;
23
+ const rel = p.startsWith(cwd + '/') ? p.slice(cwd.length + 1) : null;
24
+ return rel ?? p;
25
+ }
26
+ /**
27
+ * The READ direction of the stored-path dialect: turn a `ChapterRecord.path` back into something the
28
+ * filesystem understands. `toStoredPath` may hand back an ABSOLUTE path (the rig pins an absolute
29
+ * `artifactStoreRoot` per port — #61 item 8), and `path.join(cwd, absolute)` CONCATENATES instead of
30
+ * resolving, so a naive join sends a reader to `<cwd>/tmp/…`, which exists nowhere. Measured on the
31
+ * `enrich` leg 2026-10-24: the ladder reported `chapter file absent` about a file that was present,
32
+ * because of exactly this join.
33
+ */
34
+ export function resolveStoredPath(cwd, stored) {
35
+ if (stored.startsWith('/') || cwd === undefined || cwd === '')
36
+ return stored;
37
+ return `${cwd.replace(/\/+$/, '')}/${stored}`;
38
+ }
39
+ /** Compare two stored paths in either dialect. Every path COMPARISON goes through this, never `===`. */
40
+ export function sameStoredPath(a, b, cwd) {
41
+ if (a === b)
42
+ return true;
43
+ const root = cwd ?? '';
44
+ const abs = (p) => (p.startsWith('/') ? p : root === '' ? p : `${root}/${p}`);
45
+ return abs(a) === abs(b);
46
+ }
47
+ export const chapterFileName = (n, title) => `${String(n).padStart(3, '0')}-${slugify(title)}.md`;
48
+ /** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
49
+ /**
50
+ * The idempotence key for one archive attempt. `salt` exists for REPAIR (#60): a repair re-rendering a
51
+ * span the original door keyed would otherwise be swallowed as a retry of that attempt, and a swallowed
52
+ * repair is indistinguishable from one that worked.
53
+ */
54
+ export const attemptKey = (sourceSessionId, archiveCeiling, salt) => `${sourceSessionId}@${archiveCeiling}${salt !== undefined ? `+${salt}` : ''}`;
55
+ /** Join without importing path, so the seam stays portable between ctx.fs and node:fs. */
56
+ const join = (...parts) => parts.filter(Boolean).join('/').replace(/\/{2,}/g, '/');
57
+ export async function writeArchive(input) {
58
+ const { fs, allocator, storeRoot, rootSessionId, chapters } = input;
59
+ const cwd = input.cwd;
60
+ if (chapters.length === 0)
61
+ throw new Error('writeArchive called with no chapters');
62
+ const treeDir = join(storeRoot, rootSessionId);
63
+ const warnings = [];
64
+ // 1. Reserve BEFORE touching the filesystem, so a crash cannot re-use a number.
65
+ const numbers = await allocator.reserve(input.attemptId, chapters.length);
66
+ if (numbers.length !== chapters.length) {
67
+ throw new Error(`allocator returned ${numbers.length} numbers for ${chapters.length} chapters`);
68
+ }
69
+ const records = [];
70
+ const artifactPathsWritten = [];
71
+ const docPathsByChapter = new Map();
72
+ const seenArtifact = new Map(); // sha256 → path, for subtree dedup
73
+ const seenFile = new Set();
74
+ for (let i = 0; i < chapters.length; i++) {
75
+ const chapter = chapters[i];
76
+ const number = numbers[i];
77
+ let fileName = chapterFileName(number, chapter.range.title);
78
+ if (seenFile.has(fileName)) {
79
+ // Same title, same number prefix — disambiguate rather than let one chapter overwrite another.
80
+ fileName = fileName.replace(/\.md$/, `-${number}.md`);
81
+ warnings.push(`title collision resolved for "${chapter.range.title}" → ${fileName}`);
82
+ }
83
+ seenFile.add(fileName);
84
+ const chapterPath = join(treeDir, 'chapters', fileName);
85
+ // 2. Artifacts first: the chapter body cites these paths, so they must exist before it does.
86
+ for (const artifact of chapter.artifacts) {
87
+ const existing = seenArtifact.get(artifact.sha256);
88
+ if (existing !== undefined)
89
+ continue; // identical result in this batch
90
+ if (await fs.exists(join(treeDir, artifact.path))) { // identical result from an earlier fork
91
+ seenArtifact.set(artifact.sha256, artifact.path);
92
+ continue;
93
+ }
94
+ await fs.write(join(treeDir, artifact.path), artifact.content);
95
+ seenArtifact.set(artifact.sha256, artifact.path);
96
+ artifactPathsWritten.push(join(treeDir, artifact.path));
97
+ }
98
+ // 2b. PLAN DOCUMENTS (docs/plans-carriage.md § 11). One file per plan in this
99
+ // range, written under the SAME tree as chapters so the pool transport, the
100
+ // mirror rehydrate and the corpus index carry them with no special casing.
101
+ // Numbers come from their own registry counter — never from a directory scan.
102
+ const docs = chapter.stats.planDocs ?? [];
103
+ if (docs.length > 0 && allocator.reservePlan !== undefined) {
104
+ const numbers = await allocator.reservePlan(`${input.attemptId}#${chapterPath}`, docs.length);
105
+ const used = new Set();
106
+ for (let i = 0; i < docs.length; i++) {
107
+ const doc = docs[i];
108
+ const number = numbers[i];
109
+ // `plan-NNN-` never `NNN-plan-`: a directory listing must not read like two
110
+ // chapters claim number 001 — chapters and plan documents count separately.
111
+ let name = `plan-${String(number).padStart(3, '0')}-${slugify(doc.title)}.md`;
112
+ if (used.has(name) || seenFile.has(name))
113
+ name = name.replace(/\.md$/, `-duplicate-${number}.md`);
114
+ used.add(name);
115
+ seenFile.add(name);
116
+ const docPath = join(treeDir, 'chapters', name);
117
+ const body = `${doc.body}\n`;
118
+ const fm = [
119
+ '---',
120
+ `kind: plan`,
121
+ `title: ${JSON.stringify(doc.title)}`,
122
+ `status: ${doc.status}`,
123
+ `planSeq: ${doc.seq}`,
124
+ `chapter: ${JSON.stringify(chapterPath)}`,
125
+ // The chapter's Plans block cites the content-addressed blob; the blob and
126
+ // this document are the SAME bytes, so the sha ties the two handles together
127
+ // and a reader can prove neither drifted.
128
+ `sha256: ${doc.sha256}`,
129
+ `bodySha256: ${sha256(body)}`,
130
+ `tokens: ${doc.estTokens} # chars/4 estimate`,
131
+ `topics: [${doc.topics.map((t) => JSON.stringify(t)).join(', ')}]`,
132
+ `artifacts: 0`,
133
+ 'unrenderedSeqs: []',
134
+ '---',
135
+ '',
136
+ ].join('\n');
137
+ await fs.write(docPath, fm + body);
138
+ const read = await fs.read(docPath);
139
+ if (read === undefined || read !== fm + body) {
140
+ warnings.push(`read-back failed for the plan document, citing the chapter only: ${docPath}`);
141
+ continue;
142
+ }
143
+ artifactPathsWritten.push(docPath);
144
+ const list = docPathsByChapter.get(chapterPath) ?? [];
145
+ list.push({ seq: doc.seq, path: docPath, sha256: doc.sha256 });
146
+ docPathsByChapter.set(chapterPath, list);
147
+ }
148
+ }
149
+ // 3. The chapter itself.
150
+ await fs.write(chapterPath, chapter.markdown);
151
+ // 4. Read-back verification. A file that does not come back byte-identical is not citable.
152
+ const readBack = await fs.read(chapterPath);
153
+ if (readBack === undefined) {
154
+ warnings.push(`read-back failed (missing): ${chapterPath}`);
155
+ continue;
156
+ }
157
+ // Hash the whole file, so verifyChapter() can re-check it with no knowledge of frontmatter layout.
158
+ const bodyHash = sha256(chapter.markdown);
159
+ if (readBack !== chapter.markdown) {
160
+ warnings.push(`read-back mismatch, refusing to cite: ${chapterPath}`);
161
+ continue;
162
+ }
163
+ if (chapter.stats.unrenderedSeqs.length > 0) {
164
+ warnings.push(`${chapterPath}: ${chapter.stats.unrenderedSeqs.length} event(s) rendered no output (${chapter.stats.unrenderedSeqs.join(', ')})`);
165
+ }
166
+ if (chapter.stats.overTarget) {
167
+ warnings.push(`${chapterPath}: ${chapter.stats.estimatedTokens} tokens over chapterTokenTarget — split at the next boundary, do not clip`);
168
+ }
169
+ records.push({
170
+ number,
171
+ path: toStoredPath(cwd, chapterPath),
172
+ title: chapter.range.title,
173
+ summary: chapter.range.summary,
174
+ startSeq: chapter.range.startSeq,
175
+ endSeq: chapter.range.endSeq,
176
+ topics: chapter.topics,
177
+ messages: chapter.stats.messages,
178
+ sha256: bodyHash,
179
+ estimatedTokens: chapter.stats.estimatedTokens,
180
+ artifacts: chapter.artifacts.map((a) => ({ path: join(treeDir, a.path), sha256: a.sha256, bytes: a.bytes })),
181
+ // Plans ride along ONLY when the chapter has them: an absent field means
182
+ // "no plans here" for a pre-feature record, and an empty array here would
183
+ // be read as a fact by the notice builder.
184
+ ...(chapter.stats.plans !== undefined && chapter.stats.plans.length > 0 ? { plans: chapter.stats.plans } : {}),
185
+ ...(chapter.stats.planMode !== undefined && chapter.stats.planMode !== 'unknown' ? { planMode: chapter.stats.planMode } : {}),
186
+ ...(() => {
187
+ const docs = docPathsByChapter.get(chapterPath);
188
+ return docs !== undefined && docs.length > 0 ? { planDocs: docs } : {};
189
+ })(),
190
+ });
191
+ }
192
+ // #59: an attempt that landed NOTHING must not leave a reservation key behind forever — that is how
193
+ // numbers 20 and 37 stayed burned with no file and no record. PARTIAL landing keeps the key on
194
+ // purpose: a retry of the same attempt reuses those numbers instead of writing a second chapter
195
+ // over the same span.
196
+ if (records.length === 0 && allocator.release !== undefined) {
197
+ await allocator.release(input.attemptId);
198
+ warnings.push(`attempt ${input.attemptId} landed no chapter; its reservation key was released`);
199
+ }
200
+ return { records, artifactPathsWritten, warnings };
201
+ }
202
+ /** Coverage arithmetic: which seqs below the ceiling were NOT archived. Gaps are reported, never silent. */
203
+ export function coverage(records, archiveCeiling, archiveFloor = 0) {
204
+ const sorted = [...records].sort((a, b) => a.startSeq - b.startSeq);
205
+ const gaps = [];
206
+ // Start at the floor, not at the first chapter: a set that skips the opening of the conversation is
207
+ // exactly the silent-history-loss this function exists to catch.
208
+ let cursor = archiveFloor;
209
+ let covered = 0;
210
+ for (const r of sorted) {
211
+ if (r.startSeq > cursor)
212
+ gaps.push({ from: cursor, to: r.startSeq - 1 });
213
+ cursor = Math.max(cursor, r.endSeq + 1);
214
+ covered += r.endSeq - r.startSeq + 1;
215
+ }
216
+ if (cursor <= archiveCeiling)
217
+ gaps.push({ from: cursor, to: archiveCeiling });
218
+ return { covered, gaps };
219
+ }
220
+ /** Called before citing a chapter in an index. Detects accidental drift and single-shot injection. */
221
+ export async function verifyChapter(fs, record) {
222
+ const text = await fs.read(record.path);
223
+ if (text === undefined)
224
+ return 'missing';
225
+ if (sha256(text) !== record.sha256)
226
+ return 'modified';
227
+ return 'ok';
228
+ }
229
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ export interface ArrivalSessionShim {
2
+ surface: {
3
+ nodes: number[];
4
+ };
5
+ eventAt(seq: number): {
6
+ type: string;
7
+ data: unknown;
8
+ } | undefined | null;
9
+ append(type: string, data: unknown, opts?: unknown): unknown;
10
+ }
11
+ export declare const ARRIVAL_MARKER = "[dsh:artifact";
12
+ export interface ArrivalOpts {
13
+ cwd: string;
14
+ storeRoot: string;
15
+ floorTokens: number;
16
+ /** message → estimated tokens (host tokenMeter injected; tests use chars/4). */
17
+ estimate: (message: unknown) => number;
18
+ }
19
+ export interface ArrivalResult {
20
+ stubbed: number;
21
+ detail?: string;
22
+ }
23
+ export declare function applyArrivalStubs(session: ArrivalSessionShim, opts: ArrivalOpts): Promise<ArrivalResult>;
package/lib/arrival.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Arrival-time artifacting (record architecture.md amendment, 2026-09-19):
3
+ * tool results at or above the floor never get to become context. The blob
4
+ * goes to the content-addressed artifact store; the surface node is replaced
5
+ * IN PLACE (the pruner's proven single-node form: a shadowing event + a
6
+ * replacement append carrying `surfaceOp: replace`) with a stub that cites
7
+ * the path and the query tool. Runs at the head of compactIfNeeded/compactNow,
8
+ * i.e. inside the awaited `agent/pre-step` chain, BEFORE the next request is
9
+ * composed — so the blob is prefilled zero times, ever.
10
+ *
11
+ * A plain factory over structural shims, per house rule (the r28 lesson:
12
+ * the LOGIC must be L0-testable against the real host Session shape).
13
+ */
14
+ import { artifactPath, sha256 } from "./render.js";
15
+ import fs from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import { resolveStoreRoot } from "./store.js";
18
+ export const ARRIVAL_MARKER = '[dsh:artifact';
19
+ const kb = (n) => n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
20
+ export async function applyArrivalStubs(session, opts) {
21
+ let stubbed = 0;
22
+ // #131: resolve each result's tool name once, so an artifact ANSWER can be exempted below.
23
+ //
24
+ // Arrival-time deferral was applied to `chapters_artifact`'s own output: asking to read a stored blob returned
25
+ // ANOTHER stub pointing at ANOTHER blob, so the store became unreachable at exactly the sizes it exists for, and
26
+ // the `arrival` leg's model called the tool 20 times before the run gave up. A query answer is the payload the
27
+ // caller reached for — economizing it is a cycle, not context relief. The tool already bounds its own output per
28
+ // action (`limit`, `maxTokens`), so nothing needs a second layer here.
29
+ const nameByCall = new Map();
30
+ for (const seq of [...session.surface.nodes]) {
31
+ const ev = session.eventAt(seq);
32
+ if (ev?.type !== 'tool/call')
33
+ continue;
34
+ const d = ev.data;
35
+ if (typeof d?.callId === 'string' && typeof d?.name === 'string')
36
+ nameByCall.set(d.callId, d.name);
37
+ }
38
+ const isArtifactAnswer = (message) => {
39
+ const src = message.source;
40
+ const wrappers = Array.isArray(message.content) ? message.content : [];
41
+ const ids = [src?.callId, message.toolCallId, ...wrappers.map((w) => w?.toolCallId)];
42
+ return ids.some((id) => typeof id === 'string' && nameByCall.get(id) === 'chapters_artifact');
43
+ };
44
+ for (const seq of [...session.surface.nodes]) {
45
+ const event = session.eventAt(seq);
46
+ if (event?.type !== 'tool/result')
47
+ continue;
48
+ const data = event.data;
49
+ if (data?.message === undefined)
50
+ continue;
51
+ // REAL shape (measured e2e, 2026-09-19): message.content blocks are
52
+ // { type: 'tool-result', toolCallId, isError, content: [{type:'text',text}] }
53
+ // — the text is nested one level below the wrapper. A unit fixture that
54
+ // invented a flat shape hid this for a whole run; the fake mirrors the
55
+ // real from now on.
56
+ const blocks = Array.isArray(data.message.content) ? data.message.content
57
+ : typeof data.message.content === 'string' ? [{ type: 'text', text: data.message.content }] : [];
58
+ // TWO real shapes, one per host generation — both measured, neither assumed.
59
+ // 0.1.x: content = [{ type: 'tool-result', toolCallId, isError, content: [{type:'text',text}] }]
60
+ // 0.2.0-rc.2: content = [{ type: 'text', text }] ← flattened; measured 2026-10-22 in the
61
+ // arrival leg's own durable log (seq 51, 54,763 bytes; message keys role,source,toolCallId,
62
+ // content,isError,id; block keys type,text).
63
+ // Recognizing only the first meant `wrappers.length === 0` on EVERY result of the new host:
64
+ // nothing stubbed, `detail` unset, and no line anywhere — which is how arrival artifacting
65
+ // vanished from the whole acceptance chain while the engine's entry line proved it was running.
66
+ const wrappers = blocks.filter((b) => b?.type === 'tool-result');
67
+ const carriers = wrappers.length > 0 ? wrappers : blocks.filter((b) => b?.type === 'text' && typeof b.text === 'string');
68
+ if (carriers.length === 0)
69
+ continue;
70
+ const textOf = (w) => Array.isArray(w.content)
71
+ ? w.content.filter((c) => c?.type === 'text').map((c) => c.text ?? '').join('')
72
+ : typeof w.content === 'string' ? w.content
73
+ : typeof w.text === 'string' ? w.text : '';
74
+ const text = carriers.map(textOf).join('');
75
+ if (text.length === 0 || text.includes(ARRIVAL_MARKER))
76
+ continue;
77
+ if (isArtifactAnswer(data.message))
78
+ continue; // #131 — a pointer to a pointer is not relief
79
+ const tokens = opts.estimate(data.message);
80
+ if (tokens < opts.floorTokens)
81
+ continue;
82
+ const hash = sha256(text);
83
+ const rel = artifactPath(hash);
84
+ const abs = path.join(resolveStoreRoot(opts.cwd, opts.storeRoot), rel);
85
+ try {
86
+ await fs.mkdir(path.dirname(abs), { recursive: true, mode: 0o700 });
87
+ // content-addressed: rewriting identical bytes is a no-op by definition
88
+ await fs.writeFile(abs, text, 'utf8');
89
+ }
90
+ catch (error) {
91
+ // the blob stays inline — arrival artifacting is an optimization, and a
92
+ // disk problem must never eat the conversation (loud via detail).
93
+ return { stubbed, detail: `artifact write failed (${String(error?.message ?? error)}); ${stubbed} node(s) stubbed before the failure` };
94
+ }
95
+ const stub = `${ARRIVAL_MARKER} ${hash.slice(0, 12)}] ${kb(text.length)} ≈ ${tokens} tok stored at ${rel} — query it with chapters_artifact (path '${rel}' or the sha): action 'toc' for the heading map with line numbers, 'search' for term hits as small blocks, 'read' for exact line ranges. The full result is NOT elsewhere in this context; re-running the original tool only recreates it.`;
96
+ // the stub goes back in the shape it arrived: nested wrappers keep their `content` blocks,
97
+ // flattened v4 blocks keep their own `text` and their `type: 'text'` — a replacement that
98
+ // changed the block type would be a new shape assumption on the read side.
99
+ const stubbedCarriers = carriers.map((w, i) => (wrappers.length > 0 && Array.isArray(w.content))
100
+ ? { ...w, content: [{ type: 'text', text: i === 0 ? stub : '' }] }
101
+ : { ...w, text: i === 0 ? stub : '' });
102
+ const message = {
103
+ ...data.message,
104
+ content: [...blocks.filter((b) => !carriers.includes(b)), ...stubbedCarriers],
105
+ };
106
+ try {
107
+ session.append('compaction/prune', {
108
+ shadowedRange: { start: seq, end: seq },
109
+ shadowedSeqs: [seq],
110
+ shadowedTokenCount: tokens,
111
+ by: 'dsh-chapters-arrival',
112
+ });
113
+ session.append('tool/result', { ...data, message }, {
114
+ surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq },
115
+ sourceEventSeqs: [seq],
116
+ });
117
+ stubbed++;
118
+ }
119
+ catch (error) {
120
+ return { stubbed, detail: `surface replace refused at seq ${seq} (${String(error?.message ?? error)})` };
121
+ }
122
+ }
123
+ return { stubbed };
124
+ }
125
+ //# sourceMappingURL=arrival.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrival.js","sourceRoot":"","sources":["../src/arrival.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ export type TocEntry = {
2
+ line: number;
3
+ depth: number;
4
+ title: string;
5
+ };
6
+ /**
7
+ * Resolve a user/model-supplied reference to an absolute artifact path INSIDE
8
+ * the store. Accepts a 64-hex sha, 'artifacts/xx/<sha>.txt', or an absolute
9
+ * path. Throws on anything escaping the store — this tool is read-only, the
10
+ * containment is the whole security story.
11
+ */
12
+ export declare function resolveArtifactPath(storeDir: string, ref: string): string;
13
+ /**
14
+ * Markdown heading map with 1-based line numbers — a 300-page book arrives
15
+ * with its own table of contents (fenced code blocks are skipped so '#
16
+ * comments' inside them never masquerade as headings).
17
+ */
18
+ export declare function buildToc(text: string, maxEntries?: number): {
19
+ entries: TocEntry[];
20
+ truncated: boolean;
21
+ };
22
+ export type SearchBlock = {
23
+ start: number;
24
+ end: number;
25
+ lines: string[];
26
+ };
27
+ /**
28
+ * Case-insensitive query over lines: regex when it compiles, literal phrase
29
+ * otherwise. Matches expand to ±context lines and overlapping windows merge;
30
+ * the pack stops at maxTokens and REPORTS what it left behind.
31
+ */
32
+ export declare function searchArtifact(text: string, query: string, opts?: {
33
+ maxTokens?: number;
34
+ contextLines?: number;
35
+ }): {
36
+ total: number;
37
+ blocks: SearchBlock[];
38
+ truncated: boolean;
39
+ remaining: number;
40
+ };
41
+ /** Exact line window (1-based inclusive), capped per call; bounds reported. */
42
+ export declare function readLines(text: string, offset: number, limit: number, hardCap?: number): {
43
+ start: number;
44
+ end: number;
45
+ of: number;
46
+ lines: string[];
47
+ };
48
+ /** Load an artifact with a size guard; returns text + totals for the pack. */
49
+ export declare function loadArtifact(absPath: string): {
50
+ text: string;
51
+ bytes: number;
52
+ lines: number;
53
+ };
54
+ /**
55
+ * Store-first, mirror-second resolution (measured 2026-09-28, treeseed pool
56
+ * audit): chapters cite 'artifacts/aa/<sha>.txt' — on another machine the blob
57
+ * lives in the knowledge MIRROR (artifacts/<projectKey>/aa/<sha>.txt, one dir
58
+ * per linked project), not the local store. Returns the absolute path of the
59
+ * first existing match, or null. Containment holds on BOTH roots; '..' can
60
+ * never escape.
61
+ */
62
+ export declare function resolveArtifactWithMirror(storeDir: string, ref: string, mirrorDir: string): string | null;