@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
package/lib/store.js ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * The storage-domain adapter: zod schemas, the durable spec, and the ports
3
+ * that connect the pure registry (registry.ts) and pure archive writer
4
+ * (archive.ts) to host persistence and the filesystem. This is glue next to
5
+ * `src/index.ts` — thin by policy; every decision lives in the pure layer.
6
+ *
7
+ * Keying: one record per SESSION id (`sessions` table), because chapters are
8
+ * keyed by creating session, not a linear chain id — siblings branching from
9
+ * one ancestor cannot collide, and the flat TOC is built by walking links
10
+ * (registry.ancestorPath), never by a directory scan.
11
+ *
12
+ * Concurrency, honestly: read-modify-write per key. Same-session archive
13
+ * flows are serialized by their own reserve-before-write ordering, and a
14
+ * session has exactly one write handle (the kernel refuses a second, measured
15
+ * r3). Cross-process same-key writes are last-writer-wins on the JSON
16
+ * backend — acceptable for MVP, worth revisiting before multi-host chains.
17
+ */
18
+ import { z } from 'zod';
19
+ import { ruleRecordSchema } from "./rules.js";
20
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
21
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
22
+ import { dirname, isAbsolute } from 'node:path';
23
+ import { appendChapters, freshSession, releaseReservation, reserve, reservePlan as reservePlanNumber } from "./registry.js";
24
+ // ---------------------------------------------------------------- schemas + spec
25
+ const chapterRecordSchema = z.object({
26
+ number: z.number().int().positive(),
27
+ path: z.string().min(1),
28
+ title: z.string(),
29
+ summary: z.string(),
30
+ startSeq: z.number().int().nonnegative(),
31
+ endSeq: z.number().int().nonnegative(),
32
+ topics: z.array(z.string()).default([]),
33
+ messages: z.number().int().nonnegative().optional(),
34
+ shadowedSeqs: z.array(z.number().int().nonnegative()).optional(),
35
+ sha256: z.string().min(16),
36
+ /** P2 §2.4 provenance chains, newest-first per field (absent = everything
37
+ * still deterministic; enrichment only ever grows these). */
38
+ generated: z.record(z.string(), z.array(z.object({
39
+ by: z.string(), model: z.string().optional(), at: z.string().optional(),
40
+ }))).optional(),
41
+ estimatedTokens: z.number().int().nonnegative(),
42
+ artifacts: z.array(z.object({
43
+ path: z.string().min(1),
44
+ sha256: z.string().min(16),
45
+ bytes: z.number().int().nonnegative(),
46
+ })),
47
+ // Plans this chapter carries (docs/plans-carriage.md § 2). Optional: a stored row
48
+ // from before the feature simply has no field, which reads as "no plans here".
49
+ plans: z.array(z.object({
50
+ seq: z.number().int().nonnegative(),
51
+ status: z.enum(['approved', 'rejected', 'dismissed', 'pending', 'superseded', 'failed']),
52
+ title: z.string(),
53
+ sha256: z.string().min(16),
54
+ artifactPath: z.string().min(1),
55
+ bytes: z.number().int().nonnegative(),
56
+ estTokens: z.number().int().nonnegative(),
57
+ resultSeq: z.number().int().nonnegative().nullable(),
58
+ })).optional(),
59
+ planMode: z.enum(['active', 'inactive', 'unknown']).optional(),
60
+ // Where each plan's own `kind: plan` document landed.
61
+ planDocs: z.array(z.object({
62
+ seq: z.number().int().nonnegative(),
63
+ path: z.string().min(1),
64
+ sha256: z.string().min(16),
65
+ })).optional(),
66
+ // Tombstone status (#60 supersede, #255 operator removal) and its bookkeeping. Optional so rows
67
+ // written before these fields parse unchanged — and WITHOUT them here, zod STRIPS them on parse:
68
+ // the removed-stub round-trip test in registry.test.ts is the guard that the watermark-carrying
69
+ // fields survive every schema seam (repair's `repairs:` provenance rode the same hole).
70
+ status: z.enum(['superseded', 'removed']).optional(),
71
+ supersededBy: z.number().int().positive().optional(),
72
+ removedAt: z.string().min(10).optional(),
73
+ repairs: z.array(z.object({ class: z.string(), over: z.number().int(), at: z.number().int() })).optional(),
74
+ });
75
+ const collectionSchema = z.object({
76
+ seqs: z.array(z.number().int().nonnegative()).min(1),
77
+ paths: z.array(z.string()),
78
+ commands: z.array(z.string()),
79
+ terms: z.array(z.string()),
80
+ size: z.number().int().nonnegative(),
81
+ by: z.literal('deterministic'),
82
+ });
83
+ const sessionStateSchema = z.object({
84
+ rev: z.number().int().nonnegative().default(0),
85
+ parentSession: z.string().nullable(),
86
+ rootSession: z.string().min(1),
87
+ nextChapterNumber: z.number().int().positive(),
88
+ // Plan-document numbering (plans-carriage § 11). Optional with a default so a
89
+ // stored row from before the feature parses unchanged.
90
+ nextPlanNumber: z.number().int().positive().default(1),
91
+ chapters: z.array(chapterRecordSchema),
92
+ reservations: z.record(z.string(), z.array(z.number().int().positive())).default({}),
93
+ // #113: the Suggested Next Step bar's durable facts. Optional-with-default (the `nextPlanNumber`
94
+ // precedent) so a stored row from before the feature parses unchanged and no chapter bytes move.
95
+ promptSuggestion: z.object({
96
+ status: z.enum(['off', 'analyzing', 'ready', 'failed']),
97
+ text: z.string(),
98
+ turnSeq: z.number().int().nullable(),
99
+ error: z.string(),
100
+ at: z.string(),
101
+ intent: z.enum(['worker-step', 'goal-edit']).default('worker-step'),
102
+ goalRevision: z.number().int().nullable().default(null),
103
+ }).default({ status: 'off', text: '', turnSeq: null, error: '', at: '', intent: 'worker-step', goalRevision: null }),
104
+ plans: z.record(z.string(), z.object({
105
+ number: z.number().int().positive(),
106
+ path: z.string().min(1),
107
+ title: z.string(),
108
+ summary: z.string(),
109
+ chapters: z.array(z.object({
110
+ number: z.number().int().positive(),
111
+ path: z.string().min(1),
112
+ title: z.string(),
113
+ summary: z.string(),
114
+ startSeq: z.number().int().nonnegative(),
115
+ endSeq: z.number().int().nonnegative(),
116
+ })).optional(),
117
+ })).default({}),
118
+ finalized: z.record(z.string(), z.array(z.number().int().positive())).default({}),
119
+ collections: z.array(collectionSchema).default([]),
120
+ });
121
+ /** Parse (and backfill defaults for) one raw registry record — the seam
122
+ * migrations and tests use; the domain open path uses the same schema. */
123
+ export const parseSessionState = (raw) => sessionStateSchema.parse(raw);
124
+ const projectRecordSchema = z.object({
125
+ projectKey: z.string().min(1),
126
+ slug: z.string(),
127
+ remote: z.string().min(1),
128
+ harnessId: z.string().min(1),
129
+ linkedAt: z.string(),
130
+ cwd: z.string(),
131
+ /** Transport kind (§15 amendment); absent on stored rows = 'git'. */
132
+ kind: z.enum(['git', 'treedx']).default('git'),
133
+ /** TreeDX: server-assigned repository id, resolved at link time. */
134
+ repoId: z.string().optional(),
135
+ });
136
+ /** Parse (and default-fill) one stored project row — the migration seam tests
137
+ * and any future provider-kind backfill use; same shape as parseSessionState. */
138
+ export const parseProjectRecord = (raw) => projectRecordSchema.parse(raw);
139
+ /** Durable declaration of the dsh_chapters registry domain. */ export const chapterDomainSpec = defineDomain({
140
+ name: 'dsh_chapters',
141
+ // v1: + settings table (enrichment model overrides, P2). Existing v0 media
142
+ // load under compatibleVersions; absent tables materialize empty.
143
+ // v2: + rules table (P3 §7.1 records; status rides per-machine curation facts,
144
+ // never this table).
145
+ // v3: + projects.kind / projects.repoId (§15 amendment: transport is
146
+ // pluggable). Stored v2 rows parse with kind defaulting to 'git' — old data
147
+ // keeps working with zero migration, and the git path is untouched.
148
+ version: 3,
149
+ compatibleVersions: [0, 1, 2],
150
+ tables: {
151
+ sessions: domainTable(sessionStateSchema),
152
+ projects: domainTable(projectRecordSchema),
153
+ settings: domainTable(z.object({ value: z.string() })),
154
+ rules: domainTable(ruleRecordSchema),
155
+ },
156
+ });
157
+ export function makeDomainStore(domain) {
158
+ const table = domain.table('sessions');
159
+ const projects = domain.table('projects');
160
+ const settings = domain.table('settings');
161
+ const rules = domain.table('rules');
162
+ // One serialized section per session (#59). The row is a single whole value and five doors plus the
163
+ // turn/end signature listener write it; the wide sites hold a read across a model call or hundreds
164
+ // of filesystem writes, and an unguarded whole-row put then silently ERASES whatever another door
165
+ // committed in between (measured in the field: a compaction's manifest, reservation, `finalized`
166
+ // entry and chapter record all vanished while its 960 KB file stayed on disk, and the chapter number
167
+ // was re-spent). Serialization plus a rev check turns that into a refusal with numbers.
168
+ const sections = new Map();
169
+ const inside = new Set();
170
+ const runExclusive = async (sessionId, job) => {
171
+ if (inside.has(sessionId)) {
172
+ // Nested sections would deadlock (the inner waits on the outer's gate). Refusing is honest and
173
+ // cheap; a caller that needs to compose two mutations does it inside ONE fn.
174
+ throw new Error(`mutate: re-entered the registry section for ${sessionId} — compose the mutations in one fn`);
175
+ }
176
+ const prev = sections.get(sessionId) ?? Promise.resolve();
177
+ let release;
178
+ const gate = new Promise((r) => { release = r; });
179
+ // Chain: this session's next waiter must also wait for OUR completion, not just our predecessor.
180
+ const mine = prev.then(() => gate);
181
+ sections.set(sessionId, mine);
182
+ await prev.catch(() => undefined);
183
+ inside.add(sessionId);
184
+ try {
185
+ return await job();
186
+ }
187
+ finally {
188
+ inside.delete(sessionId);
189
+ release();
190
+ if (sections.get(sessionId) === mine)
191
+ sections.delete(sessionId);
192
+ }
193
+ };
194
+ const readRow = (sessionId) => table.get(sessionId) ?? freshSession(sessionId);
195
+ /** Bump the write counter and store. Only ever called from INSIDE a section. */
196
+ const writeInside = async (sessionId, state, currentRev) => {
197
+ await table.put(sessionId, { ...state, rev: currentRev + 1 });
198
+ };
199
+ return {
200
+ async get(sessionId) {
201
+ return readRow(sessionId);
202
+ },
203
+ async put(sessionId, state, expectedRev) {
204
+ return runExclusive(sessionId, async () => {
205
+ const current = readRow(sessionId);
206
+ if (expectedRev !== undefined && (current.rev ?? 0) !== expectedRev) {
207
+ throw new Error(`registry put refused: session ${sessionId} moved under this write (expected rev ${expectedRev}, `
208
+ + `found ${current.rev ?? 0}). Another door committed since the read — re-read and re-apply, `
209
+ + 'do not clobber it (store.mutate).');
210
+ }
211
+ await writeInside(sessionId, state, current.rev ?? 0);
212
+ });
213
+ },
214
+ async mutate(sessionId, fn) {
215
+ return runExclusive(sessionId, async () => {
216
+ const before = readRow(sessionId);
217
+ const draft = structuredClone(before);
218
+ const next = await fn(draft);
219
+ // Returning the very object handed in means "no change" — the same idiom commitChapters used
220
+ // (`if (next !== state)`) so an idempotent retry costs no write and no rev bump.
221
+ if (next === draft)
222
+ return before;
223
+ await writeInside(sessionId, next, before.rev ?? 0);
224
+ return next;
225
+ });
226
+ },
227
+ projects() {
228
+ return projects.entries();
229
+ },
230
+ sessions() {
231
+ return table.entries();
232
+ },
233
+ getSetting(key) {
234
+ return settings.get(key)?.value;
235
+ },
236
+ async putSetting(key, value) {
237
+ await settings.put(key, { value });
238
+ },
239
+ async deleteSetting(key) {
240
+ const del = settings.delete;
241
+ if (typeof del !== 'function') {
242
+ throw new Error('settings table lacks delete — cannot restore the config default (host too old)');
243
+ }
244
+ await del.call(settings, key);
245
+ },
246
+ rules() {
247
+ return rules.entries();
248
+ },
249
+ getRule(id) {
250
+ return rules.get(id);
251
+ },
252
+ async putRule(id, value) {
253
+ await rules.put(id, value);
254
+ },
255
+ };
256
+ }
257
+ /**
258
+ * The NumberAllocator archive.ts asks for, bound to one author session.
259
+ * Reserve-before-write per docs/architecture.md § Atomicity; retries reuse
260
+ * numbers through registry.reserve's attempt-keyed idempotence.
261
+ */
262
+ export function makeAllocator(store, sessionId) {
263
+ return {
264
+ // Allocation is the operation that MUST NOT interleave: two doors that both read
265
+ // `nextChapterNumber: 15` write two different `015-*.md` files into one tree, which is the exact
266
+ // damage #59 measured. `mutate` makes read-allocate-write one section, so the number a door is
267
+ // handed is already durable by the time the call returns.
268
+ async reserve(attemptId, count) {
269
+ let numbers = [];
270
+ await store.mutate(sessionId, (state) => {
271
+ const next = reserve(state, attemptId, count);
272
+ numbers = next.numbers;
273
+ return next.state;
274
+ });
275
+ return numbers;
276
+ },
277
+ async reservePlan(attemptId, count) {
278
+ let numbers = [];
279
+ await store.mutate(sessionId, (state) => {
280
+ const next = reservePlanNumber(state, attemptId, count);
281
+ numbers = next.numbers;
282
+ return next.state;
283
+ });
284
+ return numbers;
285
+ },
286
+ async release(attemptId) {
287
+ await store.mutate(sessionId, (state) => releaseReservation(state, attemptId));
288
+ },
289
+ };
290
+ }
291
+ /** The commit-point write: verified chapter records into the session state. */
292
+ export async function commitChapters(store, sessionId, records) {
293
+ // One section, fresh read inside it (#59): the old get→append→put held the snapshot across an
294
+ // await, which is how another door's committed state got erased.
295
+ await store.mutate(sessionId, (state) => appendChapters(state, records));
296
+ }
297
+ export async function acquireChapterStore(storageDomain) {
298
+ try {
299
+ const domain = await storageDomain.open(chapterDomainSpec);
300
+ return { store: makeDomainStore(domain), domain, owner: true };
301
+ }
302
+ catch (error) {
303
+ if (!/already[- ]open/i.test(String(error?.message ?? error)))
304
+ throw error;
305
+ const existing = storageDomain.get?.(chapterDomainSpec.name);
306
+ if (existing === undefined)
307
+ throw error;
308
+ return { store: makeDomainStore(existing), domain: existing, owner: false };
309
+ }
310
+ }
311
+ // ---------------------------------------------------------------- filesystem port
312
+ const join = (...parts) => parts.filter(Boolean).join('/').replace(/\/{2,}/g, '/');
313
+ /**
314
+ * ArchiveFs over node:fs, anchored at one base dir (the session workspace).
315
+ * Plugins are trusted same-process code — the sandbox governs TOOLS, not
316
+ * this path — so node:fs is legitimate here; `ctx.fs` stays untouched.
317
+ * `read` maps ENOENT to undefined; anything else propagates: a permission
318
+ * error must not look like a missing file.
319
+ */
320
+ /** Resolve a configured store root against the session cwd. An ABSOLUTE root
321
+ * wins verbatim — the hosted profiles (#33) pin `/workspace/.dsh-chapters` so
322
+ * archives land on a persisted volume no matter which subdirectory a session
323
+ * runs in; relative roots keep the historical per-workspace meaning. */
324
+ export function resolveStoreRoot(cwd, storeRoot) {
325
+ return isAbsolute(storeRoot) ? storeRoot : join(cwd, storeRoot);
326
+ }
327
+ export function makeArchiveFs(baseDir) {
328
+ // absolute paths pass through (resolveStoreRoot may hand writeArchive an
329
+ // absolute storeRoot); relative ones stay workspace-rooted as always.
330
+ const abs = (p) => (isAbsolute(p) ? p : join(baseDir, p));
331
+ return {
332
+ async write(p, content) {
333
+ const target = abs(p);
334
+ await mkdir(dirname(target), { recursive: true });
335
+ await writeFile(target, content, 'utf8');
336
+ },
337
+ async read(p) {
338
+ try {
339
+ return await readFile(abs(p), 'utf8');
340
+ }
341
+ catch (error) {
342
+ if (error.code === 'ENOENT')
343
+ return undefined;
344
+ throw error;
345
+ }
346
+ },
347
+ async exists(p) {
348
+ return (await this.read(p)) !== undefined;
349
+ },
350
+ };
351
+ }
352
+ /**
353
+ * "Add <store-root>/ to .gitignore on first write and say so" — silently
354
+ * polluting a user's tree is its own small bug (docs/architecture.md §
355
+ * Storage). Returns a human line for the tool result's warnings.
356
+ */
357
+ export async function ensureStoreGitignore(fs, storeRoot) {
358
+ const current = await fs.read('.gitignore');
359
+ const entry = `${storeRoot.replace(/\/+$/, '')}/`;
360
+ if (current !== undefined && current.split('\n').some((line) => line.trim() === entry)) {
361
+ return `${entry} already in .gitignore`;
362
+ }
363
+ const updated = (current ?? '') + (current !== undefined && !current.endsWith('\n') ? '\n' : '') +
364
+ `\n# dsh-chapters archive (verbatim chapter store; see README)\n${entry}\n`;
365
+ await fs.write('.gitignore', updated);
366
+ return `added ${entry} to .gitignore — say so: the plugin wrote into the repo root`;
367
+ }
368
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/lib/sync.d.ts ADDED
@@ -0,0 +1,220 @@
1
+ import { type ProviderKind, type SyncProvider } from './provider.ts';
2
+ import { type VocabOpts } from './vocabulary.ts';
3
+ /** A linked knowledge project (record §2.1, §8): one record per linked
4
+ * workspace cwd; deepest-cwd match resolves the active project. */
5
+ export interface ProjectRecord {
6
+ projectKey: string;
7
+ slug: string;
8
+ remote: string;
9
+ harnessId: string;
10
+ linkedAt: string;
11
+ /** Workspace cwd this record was linked from (deepest-prefix match, record §8). */
12
+ cwd: string;
13
+ /** Transport kind (§15 amendment); absent = 'git' (pre-provider records). */
14
+ kind?: ProviderKind;
15
+ /** TreeDX: the server-assigned repository id (resolved at link time). */
16
+ repoId?: string | undefined;
17
+ }
18
+ export interface SyncResult {
19
+ ok: boolean;
20
+ /** What actually happened, for the status command and tests. */
21
+ steps: string[];
22
+ detail: string;
23
+ /** 'synced' only when the remote accepted the push; 'local-only' when the
24
+ * mirror is current but the remote did not take it (record §5.3). */
25
+ mode: 'synced' | 'local-only';
26
+ }
27
+ /**
28
+ * Per-project credentials — stored under the DSH HOME, never the workspace
29
+ * (user directive 2026-09-18: the project tree is the agent's reading room;
30
+ * the plugin is the only accessor). 0600 file per projectKey. Note honestly:
31
+ * same-uid files are a hygiene boundary, not a cryptographic one — the real
32
+ * guarantee is that the plugin never echoes the token anywhere an agent sees.
33
+ */
34
+ export declare const tokenPath: (projectKey: string) => string;
35
+ export declare function readToken(cwd: string, storeRoot: string, projectKey: string): string | undefined;
36
+ export declare function writeToken(projectKey: string, token: string): void;
37
+ export interface SyncOpts {
38
+ /** Workspace root — the store and the mirror both live under it. */
39
+ cwd: string;
40
+ /** Store root relative to cwd (`.dsh-chapters`). */
41
+ storeRoot: string;
42
+ /** Mirror directory relative to cwd (`.dsh-knowledge`). */
43
+ cloneDir: string;
44
+ project: ProjectRecord;
45
+ /** Per-harness token for this project (record §2.1); undefined for file remotes. */
46
+ token?: string;
47
+ /** Collection files to (re)publish: one JSONL file per session's signatures. */
48
+ collections?: {
49
+ sessionId: string;
50
+ lines: string[];
51
+ }[];
52
+ /** P2 §6.3 vocabulary pass over the mirror's chapters (undefined = off). */
53
+ vocab?: VocabPassOpts;
54
+ /** Force a run even when the lock is held (tests). Default false. */
55
+ force?: boolean;
56
+ /** Transport provider (tests inject a fake; default resolves from the
57
+ * project record's kind via src/provider.ts — 'git' when absent). */
58
+ provider?: SyncProvider;
59
+ /** #255: names the operator removed from THIS project's store. Rehydration skips them;
60
+ * the count lands in the steps. Absent ⇒ guard inert (the pre-#255 behavior). */
61
+ removed?: RemovedGuard;
62
+ }
63
+ /**
64
+ * Deepest-cwd match (record §8): the active knowledge project for a
65
+ * workspace path. Shared by the notice line, the scheduler, and the commands.
66
+ */
67
+ export declare function projectForCwd(records: Iterable<[string, ProjectRecord]>, cwd: string): ProjectRecord | undefined;
68
+ /** The mirror directory, relative to the workspace (record §3.1). */
69
+ export declare const DEFAULT_CLONE_DIR = ".dsh-knowledge";
70
+ /**
71
+ * File lock around store transactions and clone ops (§5.2): the host-plane
72
+ * plugin and the realm engine may be different module instances in the same
73
+ * process, so an in-process mutex is not safe — a file is the honest lock.
74
+ * Stale locks (>10 min) are stolen; live ones make runSync skip, not block.
75
+ */
76
+ export declare function syncLockPath(storeDir: string): string;
77
+ /**
78
+ * Map workspace-store files to repo paths (record §2.2):
79
+ * <storeRoot>/<rootSession>/chapters/x.md → chapters/<projectKey>/<rootSession>/x.md
80
+ * <storeRoot>/<rootSession>/artifacts/ab/ab….txt → artifacts/<projectKey>/ab/ab….txt
81
+ * Artifacts are content-addressed and shared across sessions, so their
82
+ * per-session prefix is dropped; chapters keep it.
83
+ */
84
+ export declare function planStoreToRepo(storeDir: string, projectKey: string): {
85
+ abs: string;
86
+ rel: string;
87
+ }[];
88
+ export declare const statusFilePath: (cwd: string, storeRoot: string) => string;
89
+ /**
90
+ * Build the Layer-2 index inside the clone (record §10.2): entries from every
91
+ * chapter/rule file (frontmatter), curation facts from every harness's
92
+ * curation.jsonl, the manifest from the clone's index/manifest.json. Returns
93
+ * true when shards or the manifest changed (the commit step then ships them).
94
+ * Deterministic: the index is a pure function of the clone's corpus, so any
95
+ * machine can rebuild it identically (the recovery path, record §10.2).
96
+ */
97
+ export declare function buildIndexInClone(cloneDir: string): boolean;
98
+ export interface VocabPassOpts extends VocabOpts {
99
+ /** write applied aliases? false (shadow) = report only — the default */
100
+ apply: boolean;
101
+ }
102
+ /**
103
+ * One sync pass (record §5): lock → ensure clone (offline init fallback) →
104
+ * publish → commit → pull → push. Failure semantics §5.3: EVERY remote-path
105
+ * failure (clone, pull, push) degrades to local-only mode — the mirror stays
106
+ * current for search and the status file says so with detail; only a local
107
+ * commit failure or lock loss returns without a current mirror. A diverged
108
+ * pull rebuilds the mirror from the remote (safe: the mirror is transport,
109
+ * the store is truth, §3.1). ok=true means the remote accepted the push.
110
+ */
111
+ /**
112
+ * Store rehydration (0.1.5 — treeseed workspace-wipe incident, 2026-10-08):
113
+ * the pool is not only a downstream copy. When a workspace store loses files
114
+ * (accidental rm of dot-dirs, cleanup, fresh checkout) the registry rows and
115
+ * continuation TOCs still cite the store paths — `read` dangles silently.
116
+ * The freshly cloned/pulled mirror is usually the ONLY surviving copy of
117
+ * those exact bytes, so every sync restores before it publishes:
118
+ * mirror chapters/<projectKey>/<session>/<file>.md → <storeRoot>/<session>/chapters/<file>
119
+ * mirror artifacts/<projectKey>/<path> → <storeRoot>/artifacts/<path>
120
+ * Only ABSENT files are filled (the store stays ground truth for anything it
121
+ * still holds — rehydration fills gaps, it never arbitrates drift); other
122
+ * projects' prefixes are out of scope; counts land in the sync steps so the
123
+ * recovery is never silent.
124
+ *
125
+ * #255: an operator REMOVAL deletes the store file with intent. Without a guard, the next pass
126
+ * would copy the still-present mirror file straight back over the operator's decision — the
127
+ * deletion resurrecting itself between unlink and push. `removed` names those files (session#file
128
+ * for chapters and plan docs, blob basename for artifacts) and rehydration skips them, counting
129
+ * every skip so the pass says what it refused.
130
+ */
131
+ export interface RemovedGuard {
132
+ /** `<sessionDir>#<file>` — the mirror walk's identity for a chapter or plan document. */
133
+ chapterKeys: ReadonlySet<string>;
134
+ /** Artifact basenames (`<sha>.txt`) — content-addressed, unique by construction. */
135
+ blobKeys: ReadonlySet<string>;
136
+ }
137
+ export declare function rehydrateStoreFromMirror(cloneDir: string, storeDir: string, projectKey: string, removed?: RemovedGuard): {
138
+ restored: number;
139
+ skippedRemoved: number;
140
+ };
141
+ export declare function runSync(opts: SyncOpts): Promise<SyncResult>;
142
+ /**
143
+ * Pull-only pass for the pre-fork refresh point (record §5.1): ensure the
144
+ * mirror exists (clone, offline-init fallback) and ff-pull it. Never throws,
145
+ * never pushes, never commits — a read-side refresh.
146
+ */
147
+ export declare function runPull(opts: Omit<SyncOpts, 'force'>): Promise<{
148
+ ok: boolean;
149
+ detail: string;
150
+ }>;
151
+ /**
152
+ * A workspace-scoped collections reader for the scheduler (§5): only session
153
+ * trees whose store directory lives in THIS workspace publish. Shared by the
154
+ * host plugin and the realm engine (the two schedulers are the same code over
155
+ * different module copies — the file lock keeps them honest).
156
+ */
157
+ export declare function makeCollectionsReader(sessions: () => IterableIterator<[string, import('./registry.ts').SessionState]>, storeRoot: string): (cwd: string) => {
158
+ sessionId: string;
159
+ lines: string[];
160
+ }[];
161
+ /**
162
+ * The debounced scheduler (§5.2: "consecutive archive events within the
163
+ * configured window coalesce into one push"). Pure wiring — the plane that
164
+ * owns it (host plugin or realm engine) injects how to find the project, the
165
+ * token, and the collection files. Never throws; a failed pass lands in the
166
+ * status file like any other.
167
+ */
168
+ export interface SyncSchedulerDeps {
169
+ storeRoot: string;
170
+ cloneDir?: string;
171
+ debounceMs: number;
172
+ resolveProject(cwd: string): ProjectRecord | undefined;
173
+ tokenFor(cwd: string, projectKey: string): string | undefined;
174
+ /** Collection JSONL lines per session, read from the registry (may be empty). */
175
+ collectionsFor(cwd: string): {
176
+ sessionId: string;
177
+ lines: string[];
178
+ }[];
179
+ /** #255: the removed-stub guard for this cwd's project, read from the registry rows.
180
+ * The planes that own a store handle supply it; a scheduler without one simply never
181
+ * guards, and the pass says so nowhere it cannot know. */
182
+ removedFor?(cwd: string): RemovedGuard | undefined;
183
+ /** P2 §6.3 vocabulary pass options for every scheduled pass (undefined = off). */
184
+ vocab?: VocabPassOpts;
185
+ /** Injectable timer (tests). Defaults to setTimeout/clearTimeout. */
186
+ setTimer?: (fn: () => void, ms: number) => {
187
+ cancel(): void;
188
+ };
189
+ provider?: SyncProvider;
190
+ /** P2: fired after a REAL sync pass completes (success or degraded) — the
191
+ * enrichment queue's natural idle point. Not fired for 'no project
192
+ * linked' early-outs (nothing was published, nothing to enrich). */
193
+ onSyncDone?: (cwd: string, ok: boolean) => void;
194
+ }
195
+ export interface SyncScheduler {
196
+ /** Debounced push after an archive event (compaction/fork). */
197
+ schedule(cwd: string, why: string): void;
198
+ /** Immediate pass (a link, an explicit sync). An explicit project wins
199
+ * over the cwd lookup — a re-link must sync the NEW record, not whichever
200
+ * record the cwd tie-break happens to pick. */
201
+ run(cwd: string, why: string, project?: ProjectRecord): Promise<SyncResult>;
202
+ /** Pre-fork refresh (§5.1): pull-only, bounded, never blocks long. */
203
+ pullFor(cwd: string): Promise<{
204
+ ok: boolean;
205
+ detail: string;
206
+ }>;
207
+ hasPending(cwd: string): boolean;
208
+ /** Await any in-flight/scheduled pass (tests + shutdown). */
209
+ drain(): Promise<void>;
210
+ }
211
+ export declare function createSyncScheduler(deps: SyncSchedulerDeps): SyncScheduler;
212
+ /** Read the last sync status (for /chapters-status). Null when never run. */
213
+ export declare function readSyncStatus(cwd: string, storeRoot: string): {
214
+ at: string;
215
+ projectKey: string;
216
+ lastOk: boolean;
217
+ mode?: string;
218
+ detail: string;
219
+ steps: string[];
220
+ } | null;