@treeseed/dsh-chapters 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 (119) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +644 -0
  3. package/cordis.patch.yml +88 -0
  4. package/docs/architecture.md +460 -0
  5. package/docs/contract.md +375 -0
  6. package/docs/development.md +437 -0
  7. package/docs/host-compaction-seam.md +117 -0
  8. package/docs/knowledge-repo.md +730 -0
  9. package/docs/provider.md +98 -0
  10. package/docs/verify.md +200 -0
  11. package/lib/archive.d.ts +88 -0
  12. package/lib/archive.js +114 -0
  13. package/lib/archive.js.map +1 -0
  14. package/lib/arrival.d.ts +23 -0
  15. package/lib/arrival.js +86 -0
  16. package/lib/arrival.js.map +1 -0
  17. package/lib/artifact-query.d.ts +53 -0
  18. package/lib/artifact-query.js +130 -0
  19. package/lib/artifact-query.js.map +1 -0
  20. package/lib/client.js +234 -0
  21. package/lib/client.js.map +1 -0
  22. package/lib/commands.d.ts +28 -0
  23. package/lib/commands.js +305 -0
  24. package/lib/commands.js.map +1 -0
  25. package/lib/compose.d.ts +30 -0
  26. package/lib/compose.js +131 -0
  27. package/lib/compose.js.map +1 -0
  28. package/lib/continue-core.d.ts +153 -0
  29. package/lib/continue-core.js +252 -0
  30. package/lib/continue-core.js.map +1 -0
  31. package/lib/engine-core.d.ts +219 -0
  32. package/lib/engine-core.js +405 -0
  33. package/lib/engine-core.js.map +1 -0
  34. package/lib/engine.d.ts +211 -0
  35. package/lib/engine.js +571 -0
  36. package/lib/engine.js.map +1 -0
  37. package/lib/enrich-queue.d.ts +66 -0
  38. package/lib/enrich-queue.js +100 -0
  39. package/lib/enrich-queue.js.map +1 -0
  40. package/lib/enrich-store.d.ts +31 -0
  41. package/lib/enrich-store.js +103 -0
  42. package/lib/enrich-store.js.map +1 -0
  43. package/lib/enrich-wire.d.ts +73 -0
  44. package/lib/enrich-wire.js +209 -0
  45. package/lib/enrich-wire.js.map +1 -0
  46. package/lib/enrich.d.ts +73 -0
  47. package/lib/enrich.js +150 -0
  48. package/lib/enrich.js.map +1 -0
  49. package/lib/gitops.d.ts +107 -0
  50. package/lib/gitops.js +539 -0
  51. package/lib/gitops.js.map +1 -0
  52. package/lib/index.d.ts +81 -0
  53. package/lib/index.js +305 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/indexing.d.ts +85 -0
  56. package/lib/indexing.js +212 -0
  57. package/lib/indexing.js.map +1 -0
  58. package/lib/injections.d.ts +66 -0
  59. package/lib/injections.js +83 -0
  60. package/lib/injections.js.map +1 -0
  61. package/lib/notice.d.ts +60 -0
  62. package/lib/notice.js +84 -0
  63. package/lib/notice.js.map +1 -0
  64. package/lib/provider.d.ts +44 -0
  65. package/lib/provider.js +66 -0
  66. package/lib/provider.js.map +1 -0
  67. package/lib/redact.d.ts +24 -0
  68. package/lib/redact.js +85 -0
  69. package/lib/redact.js.map +1 -0
  70. package/lib/registry.d.ts +120 -0
  71. package/lib/registry.js +130 -0
  72. package/lib/registry.js.map +1 -0
  73. package/lib/render.d.ts +40 -0
  74. package/lib/render.js +280 -0
  75. package/lib/render.js.map +1 -0
  76. package/lib/repo.d.ts +78 -0
  77. package/lib/repo.js +184 -0
  78. package/lib/repo.js.map +1 -0
  79. package/lib/rules-commands.d.ts +24 -0
  80. package/lib/rules-commands.js +138 -0
  81. package/lib/rules-commands.js.map +1 -0
  82. package/lib/rules.d.ts +90 -0
  83. package/lib/rules.js +185 -0
  84. package/lib/rules.js.map +1 -0
  85. package/lib/search.d.ts +48 -0
  86. package/lib/search.js +152 -0
  87. package/lib/search.js.map +1 -0
  88. package/lib/signature.d.ts +28 -0
  89. package/lib/signature.js +149 -0
  90. package/lib/signature.js.map +1 -0
  91. package/lib/store.d.ts +130 -0
  92. package/lib/store.js +235 -0
  93. package/lib/store.js.map +1 -0
  94. package/lib/sync.d.ts +183 -0
  95. package/lib/sync.js +719 -0
  96. package/lib/sync.js.map +1 -0
  97. package/lib/tools.d.ts +84 -0
  98. package/lib/tools.js +581 -0
  99. package/lib/tools.js.map +1 -0
  100. package/lib/treedx/client.d.ts +57 -0
  101. package/lib/treedx/client.js +82 -0
  102. package/lib/treedx/client.js.map +1 -0
  103. package/lib/treedx/provider.d.ts +39 -0
  104. package/lib/treedx/provider.js +479 -0
  105. package/lib/treedx/provider.js.map +1 -0
  106. package/lib/treedx/state.d.ts +27 -0
  107. package/lib/treedx/state.js +65 -0
  108. package/lib/treedx/state.js.map +1 -0
  109. package/lib/types.d.ts +100 -0
  110. package/lib/types.js +19 -0
  111. package/lib/types.js.map +1 -0
  112. package/lib/vocabulary.d.ts +41 -0
  113. package/lib/vocabulary.js +118 -0
  114. package/lib/vocabulary.js.map +1 -0
  115. package/package.json +132 -0
  116. package/presets/chapters/agent.cordis.yml +272 -0
  117. package/presets/chapters/preset.yml +3 -0
  118. package/presets/probe-roots/agent.cordis.yml +11 -0
  119. package/presets/probe-roots/preset.yml +3 -0
@@ -0,0 +1,98 @@
1
+ # Knowledge Transport Providers
2
+
3
+ The sync layer's transport is pluggable (governing record: `knowledge-repo.md` §3.2 extended by
4
+ Amendment 2026-09-21). One project record picks one provider; nothing else knows the difference.
5
+ This file is the contract: what a provider must do, which invariants it inherits, and how each
6
+ backend fills them.
7
+
8
+ ## The seam
9
+
10
+ `runSync`'s transport surface is exactly six verbs — a provider is those, plus identity:
11
+
12
+ ```ts
13
+ interface SyncProvider extends GitDriver { // src/provider.ts
14
+ readonly kind: 'git' | 'treedx'
15
+ describe(project: ProjectRecord): string // one honest status line
16
+ ensureClone(dir, remote, opts?) // make the mirror exist and reflect the remote
17
+ initLocal(dir, remote?, opts?) // offline mirror (§5.3)
18
+ removeMirror(dir) // destroy transport (the store is truth, §3.1)
19
+ stageAllAndCommit(dir, message, author) // LOCAL diff workdir-vs-known-baseline
20
+ pullFastForward(dir, remote, opts?) // converge; failure classes diverged|network|auth
21
+ push(dir, remote, opts?) // ship the staged delta
22
+ }
23
+ ```
24
+
25
+ Rules a provider must honor (all inherited from the record, restated at the seam):
26
+
27
+ 1. **The mirror directory exists in every mode.** The workspace store is the read path's truth
28
+ (§3.1); the mirror under `.dsh-knowledge/` is materialized content — search (`loadCorpus`),
29
+ rules (`collectMirrorRules`), the index build, and the notice layers consume it unchanged.
30
+ A provider that skips materialization breaks all of them.
31
+ 2. **Every failure is a value.** `{ok:false, code}` — the machine-readable classes drive
32
+ runSync's branching: `network` (degrade to local-only), `diverged` (rebuild the mirror from
33
+ the store and re-push), `rejected` (pull-and-retry once), `origin-mismatch` (rebuild; never
34
+ sync into the wrong pool), `auth` (surface the re-link instruction with the detail). Providers
35
+ branch on status + code, **never on prose** (r35).
36
+ 3. **ff-only, append-only.** Divergence is never merge-resolved; it is rebuilt
37
+ (§5.2). A provider never rewrites remote content.
38
+ 4. **Nothing throws into the conversation path**, and no token bytes appear in any `detail`
39
+ string (§2.1).
40
+ 5. **Provider selection is scheme-first, policy-strict.** `treedx+<url>/<repo>` → TreeDX;
41
+ anything else → git. `knowledgeProvider: 'git'|'treedx'` forces one and REFUSES a
42
+ contradiction rather than falling back — a wrong pool is a data-privacy failure, not a
43
+ config convenience (`providerKindFor`).
44
+
45
+ ## Backend truth table
46
+
47
+ | Verb | git (`gitops.ts`) | TreeDX (`treedx/provider.ts`) |
48
+ |---|---|---|
49
+ | ensureClone | clone (or adopt existing repo + origin sentinel) | resolve repo in the catalog; fetch refs + `paths/list` (no extension filter — the mirror must be faithful) + `files/read`/`blobs/read`; materialize; write `.treedx-state.json` |
50
+ | initLocal | `git init` + origin | state file with `offline: true` (publish/commit/index/search keep working) |
51
+ | stageAllAndCommit | workdir-vs-HEAD content diff → git commit objects | workdir-vs-baseline diff → staged path list in the state file (baseline advances at stage time: restaging identical bytes says 'nothing to commit', the git parity) |
52
+ | pullFastForward | fetch, ancestor checks, ff; non-ff ⇒ `diverged` | head compare vs state. Unmoved ⇒ up to date. Moved, nothing staged ⇒ refetch (ff). Moved with staged work ⇒ `diverged` — a merge could leave derived files (index/, topics/) built against a stale corpus; the rebuild re-derives |
53
+ | push | git push (rejected ⇒ pull-retry) | workspace-create (`refs/heads/main`, one writable lease per branch) → write each staged path (UTF-8 File API; binary via Blob API) → commit. 409 with head unmoved ⇒ `rejected` (lease), bounded retry; 409 after the head moved ⇒ `diverged`; 401 ⇒ `auth` |
54
+ | offline recovery | remote returns → pull diverged → rebuild | `state.offline` + reachable ⇒ `diverged` (same rebuild-from-store path) |
55
+ | origin drift | `.git/DSH-ORIGIN` sentinel | `state.origin` vs the record's remote ⇒ `origin-mismatch` |
56
+
57
+ ## State: `.treedx-state.json` (the TreeDX mirror's `.git/`)
58
+
59
+ `{origin, head, baseline: {path → sha256}, staged: {message, author, paths} | null, offline, repoId?}`
60
+ — lives inside the mirror, starts with a dot so it never walks into any commit, and is the
61
+ provider's memory across verbs (provider instances stay stateless per call, safe to share).
62
+
63
+ ## Identity and credentials (§2.1/§2.3, one rule for both providers)
64
+
65
+ `projectKey = UUIDv5(NAMESPACE_OID, "treedx/" + canonicalize(baseUrl/repoName))` for TreeDX;
66
+ the unchanged `canonicalize(remote)` for git. The `treedx/` namespace keeps a service whose URL
67
+ happens to canonicalize like a git remote from stealing another's pool. Credentials ride the
68
+ same DSH-home store (`credentials/<projectKey>`, 0600); for TreeDX the token is ALWAYS required
69
+ (no loopback exemption: TreeDX answers 401, and a service URL without auth is a config bug,
70
+ not a local file).
71
+
72
+ ## What a provider may NOT touch
73
+
74
+ The workspace store, the registry domain, chapter rendering, redaction, the injection screen
75
+ (`src/injections.ts`), composition, budgets, notice assembly, and the rules-approval gate. The
76
+ approval (§15 amendment: per-machine curation fact) is the trust boundary shared state passes
77
+ through a human on the local machine — a server (TreeDX included, federation included) cannot
78
+ pre-approve anything, because the fact file lives under `edits/<harness-id>/` and the effective
79
+ status is computed locally.
80
+
81
+ ## TreeDX API surface consumed (spikes/treedx/FINDINGS.md carries the pinning)
82
+
83
+ `GET /health`, `POST /auth/dev-token` (dev), `GET|POST /repos`, `GET /repos/:id/refs`,
84
+ `POST /repos/:id/paths/list`, `POST /repos/:id/files/read`, `POST /repos/:id/blobs/read`,
85
+ `POST /repos/:id/workspaces`, `PUT|DELETE /workspaces/:id/files`, `POST /workspaces/:id/blobs/write`,
86
+ `POST /workspaces/:id/commit`. Server-side search/graph/context (`files/search`, `graph/*`,
87
+ `context/build`) and federation are Phase F: the plugin codes to one ingress URL and federation
88
+ needs no client change.
89
+
90
+ ## Failure semantics summary (§5.3, per backend)
91
+
92
+ | Condition | git | TreeDX |
93
+ |---|---|---|
94
+ | remote down | offline mirror init; push deferred | offline state; push deferred; pull probes recovery → diverged → rebuild |
95
+ | credential bad/revoked | 401 as network class + detail | `auth` class: status says "re-link the token with /chapters-link", conversation untouched |
96
+ | repo missing | GitHub creates on first push for scoped tokens (config'd) | link command creates it; sync-time absence ⇒ `not_found`, degraded with the name in the detail |
97
+ | oversized file | n/a (packs stream) | File API 413 ⇒ refuse-with-numbers detail (§1.2), never truncate; >1 MiB belongs in multipart (Phase F) |
98
+ | lease busy (two machines at once) | push rejected → pull → retry | create/commit 409 → bounded provider retry → `rejected` → runSync's pull-and-retry |
package/docs/verify.md ADDED
@@ -0,0 +1,200 @@
1
+ # Verification
2
+
3
+ The checklist in `AGENTS.md` is short because it only lists what matters. This is how to actually
4
+ perform each check, with the numbers to record.
5
+
6
+ ## Test Strategy First
7
+
8
+ Decide the harness in Phase 0, before Phase 1 code. Both reference plugins test their own way:
9
+ `dsh-compact` uses vitest (`vitest.config.ts`, `test/`, `e2e/`), `dsh-session-fork` uses `bun test`
10
+ with `tests/`.
11
+
12
+ The point of choosing early: several core guarantees are *pure functions* and must be unit-tested, not
13
+ hand-verified on a live profile every time.
14
+
15
+ | Unit-testable without a running harness | Why it must be automated |
16
+ |---|---|
17
+ | Range validation (ascending, non-overlapping, coverage ≤ ceiling) | A silent gap drops archived history forever |
18
+ | Chapter renderer: event slice → Markdown, including fence-escaping and artifact deferral | Deterministic input/output; the only cheap way to catch a renderer truncating at an embedded ```` ``` ```` |
19
+ | `anchoredBoundaryOf` → archive ceiling | Vendored kernel logic; drift is invisible until a chain is corrupted |
20
+ | TOC assembly from a fixture registry (flat, chronological, back-links) | Proves the DAG flattening without a live session |
21
+ | Preflight budget math and refusal message | The "never truncate" guarantee is one `if` away from being lost |
22
+
23
+ Kernel-dependent behaviour (durable create, sidebar listing, cache behaviour) cannot be unit-tested and
24
+ needs the live procedure below.
25
+
26
+ ## Live Procedure
27
+
28
+ Install for every step, after building:
29
+
30
+ ```bash
31
+ tsc -p tsconfig.json
32
+ dsh plugin --profile web add "link:$(pwd)"
33
+ # restart: Ctrl-C, then `dsh web`, then refresh the page
34
+ ```
35
+
36
+ A restart recomposes every session's header, which resets cache state profile-wide. **All cache
37
+ measurements are only meaningful within one process generation** — record the generation (restart) with
38
+ the numbers, or the measurement is unreproducible.
39
+
40
+ ### The Gate Checks
41
+
42
+ **G1 — The child is smaller than the parent.** *The* regression test for the seeding mistake.
43
+ Record the first-request `usage` of both. Expected: child ≈ system prompt + TOC + note, and **materially
44
+ below** the parent's. A child near the parent's size means it was seeded from the log — invariant 2
45
+ violated. Every other check can pass while this one fails silently, so check it first.
46
+
47
+ **G2 — The parent's cache is undisturbed.** Continue the parent; record `usage.cacheReadTokens` on the
48
+ step before the continuation and the step after. Expected: both high, no drop.
49
+
50
+ **G3 — The child builds a stable cache.** Send two messages in the child; `cacheReadTokens` should be
51
+ near-zero on the first and high on the second.
52
+
53
+ Steps 4, 7, 8 of the `AGENTS.md` list are G1–G3. They are the whole point of the design.
54
+
55
+ ### Correctness Checks
56
+
57
+ | # | Check | Procedure | Passes when |
58
+ |---|---|---|---|
59
+ | 1 | Installs | `dsh plugin --profile web add "link:$(pwd)"` | no error; note that `add` alone does not activate |
60
+ | 2 | Tools listed | restart, open an agent session | `chapters_segment` and `chapters_continue` in the tool list |
61
+ | 3 | Files written | run a continuation on a 2-topic conversation | chapter files exist under `<store-root>/<root>/chapters/` |
62
+ | 4 | **G1** | see above | child first request materially smaller |
63
+ | 5 | TOC is the first message | open the child | its entire content is the TOC notice; nothing inherited |
64
+ | 6 | Durable and listed | restart `dsh web` | child still in the sidebar, still resumable |
65
+ | 7 | **G2** | see above | parent `cacheReadTokens` unchanged |
66
+ | 8 | **G3** | see above | child cache builds on turn 2 |
67
+ | 9 | Reachability | in the child, ask it to `read` a bullet path | returns real archived text. A **reachability** test, not file-existence — pass `ls` first to see the difference |
68
+ | 10 | Artifact deferral | archive a turn with a large tool result; mark it `inline: false`, then `read` the reference path | artifact written and path resolves; identical results stored once; **and** the same result is still written when inlined (deferral is a copy decision, not a data decision) |
69
+ | 11 | Cumulative TOC | a second continuation | first fork's chapters still listed, by ancestry walk |
70
+ | 12 | Sibling branches | continue an ancestor twice | two correct, non-colliding TOCs, each listing only its own ancestor path |
71
+ | 13 | Registry survives restart | restart, continue again | ancestry and next number resume; no reset to `001` |
72
+ | 14 | Atomicity | kill `dsh web` mid-continuation, restart, retry the same call | no orphan citation, no duplicate numbers, no TOC pointing at a missing file |
73
+ | 15 | Range validation | hand it overlapping ranges and a deliberate gap | refused with the offending range named — not silently archived wrong |
74
+ | 16 | Preflight refusal | force a note large enough to blow the budget | refused **before** any session is created, with token numbers; **no truncation** |
75
+ | 17 | Tamper detection | edit a chapter body on disk, then continue | `⚠ modified since archived` surfaced from the stored hash, not silent trust |
76
+ | 18 | `ctx.fs` absent | run in a configuration without the fs provider | clean refusal with an actionable message, not a crash |
77
+ | 19 | Turn boundary | attempt a continuation with an open turn | ceiling lands on a completed `turn/end`; the in-flight exchange is carried by the handoff note, not by a mid-turn cut |
78
+ | 20 | First-turn behaviour | open a fresh child, wait | confirm whether it acknowledges its own TOC (open question 2). If it does, the notice needs a stay-silent instruction |
79
+
80
+ ### Evidence Map (as of probe round 26/26b, 2026-09-17)
81
+
82
+ Checks 1-19 were executed mechanically on the `.dshdev2` scratch profile
83
+ (`spikes/probe/lib/round26*.js`, `results-26*.json`): 18 of 20 PASS with durable evidence — including
84
+ [15] overlap refusals naming the range, [16] budget refusals carrying numbers, [17] a tampered chapter
85
+ surfacing `⚠ modified since archived` inside a real child's notice, [14] an idempotent retry reusing
86
+ numbers `[1, 2]` with no duplicate chapters, [13] registry state read back across a process kill, [6]
87
+ continuation children AND fork siblings resuming across boots, [19]/[20] turn-boundary refusal and
88
+ creation-schedules-no-turn. [7][8][9] (cache gates) point at r11/r22/r23 evidence; [10] boot-level rests
89
+ on its L0 tests plus the citation-rule test; [18] is N/A because the store uses node:fs by design.
90
+ **Genuinely human-only:** sidebar/visual resume in a browser, `/compact` observed in a live UI, the
91
+ readability judgment of TOC prose and chapter bodies — plus optionally following one real 40KB tool
92
+ result into an artifact.
93
+
94
+ ## Knowledge Layer — P1 Checklist (record §13)
95
+
96
+ | # | Check | Passes when | Evidence |
97
+ |---|---|---|---|
98
+ | K1 | Project identity | same repo URL, different spellings → one key; non-git cwd degrades honestly; local-path upstreams key off the resolved path | unit `repo.test.ts` (10) + `sync-local-upstream.test.ts` |
99
+ | K2 | Redaction at the chokepoint | a secret-bearing transcript never lands in a chapter or artifact, same secret → same marker | unit `redact.test.ts` + render chokepoint test; FINDINGS-era |
100
+ | K3 | Turn signatures, live | a completed real turn yields a signature with paths/terms from HUMAN content only | **live probe r35j (K5b)** + L0 `engine-adapter.test.ts` (real Session shape) |
101
+ | K4 | Topic-sequential composition | same-topic adjacent turns merge into one chapter; topic changes split; size cap splits | **live r36: merge proven (score 0.171 < τ, shared primary)**; L0 compose/anchor tests |
102
+ | K5 | Index | shards deterministic; incremental manifest gates commits; rebuild-from-empty == incremental | unit `indexing.test.ts` (7) |
103
+ | K6 | Search | topic>title>summary ranking, budget packing with total-vs-shown honesty, project scoping | unit `search.test.ts`; live r35j (K7: finds through a LOCAL-ONLY mirror) |
104
+ | K7 | Commands context-free | `/chapters-link` (4 modes: view / path / git-URL+token / `treedx+`URL+token), `/chapters-status` render as flow nodes; the model's context is unchanged | durable: `command/run`+`command/done` lifecycle events, no model/usage around them (e2e plane); r35j K2/K8; treedx modes: `link-treedx.test.ts` |
105
+ | K8 | Sync loop, success path | two machines over a REAL git smart-HTTP remote: publish → remote carries layout+index → clone sees it → search crosses | integration `sync-remote.test.ts` + **e2e `knowledge.spec.ts` — suite green 2026-09-18 (4/4, twice — also after the credential-move + three-mode link)**: browser composer link pushes to a live git-http-backend remote, status reaches 'synced', the child TOC carries the Project line + (N msgs), the debounced push lands `collections/`, and machine B searches A's chapters |
106
+ | K9 | Sync loop, degradation | remote down ⇒ local-only mode (mirror+index current, status names it); recovery rebuilds and pushes | `sync-remote.test.ts` degradation arc; **live r35j: link to unreachable remote, search still works, status 'local-only'** |
107
+ | K10 | Triggers fire | push scheduled after each archive (debounced); pull on first turn + pre-fork | **proven by K8's e2e run**: `collections/` reached the remote with no manual sync between fork and poll; L0 `scheduler.test.ts` + first-turn tests |
108
+ | K11 | Notice v2 | `Project:` line + per-chapter `(N msgs)`; legacy records never render a fake zero | r35j (K6, K6b); `notice.test.ts` |
109
+
110
+ Human-only rows (unchanged): sidebar visual resume, `/compact` observed in a live UI, TOC/chapter
111
+ readability judgment.
112
+
113
+ ## Knowledge Transport Providers (record §15, Amendments 2026-09-21)
114
+
115
+ | # | Check | Passes when | Evidence |
116
+ |---|---|---|---|
117
+ | T1 | Seam preserves git behavior | provider selection defaults 'git' for every pre-existing record; all K1–K10 evidence still green after the refactor | full suite (276→285) with zero git-mode changes; `provider.test.ts` selection + migration rows |
118
+ | T2 | TreeDX publish/clone/search | machine A syncs (create→workspace→commit), B clones via the provider and `chapters_search` finds A's chapter through the materialized mirror | `treedx-sync.test.ts` (transport grade, stub) + **`treedx-live.test.ts` PROVEN against the real dev-auth container 2026-09-21** (born-head clone, publish→clone→search crossing, bad-token auth detail, head advance/lease reuse, binary blob round-trip) |
119
+ | T3 | Divergence & lease classes | moved head + staged work ⇒ `diverged` ⇒ rebuild-from-store republishes BOTH contributions; busy lease ⇒ bounded retry ⇒ `rejected` ⇒ pull-and-retry; failed push CLOSES its workspace (leases survive ttl otherwise — measured) | `treedx-provider.test.ts` (all three), `treedx-sync.test.ts` (rebuild converge) |
120
+ | T4 | Degradation with numbers | service down / token bad ⇒ `local-only` (offline mirror still publishes, commits, indexes, searches); `auth` detail carries the re-link instruction, never the token | `treedx-sync.test.ts` offline row; `treedx-provider.test.ts` auth row (token-sentinel absence) |
121
+ | T5 | Oversized refusal | a UTF-8 file above the service limit ⇒ refuse with the number, never truncate | `treedx-provider.test.ts` 1 MiB-413 row |
122
+ | T6 | Identity namespaces | `treedx+host/repo` key ≠ a git remote's key for the same canonical string (one pool per transport); stable across spellings (`/api/v1`, trailing slash, case) | `provider.test.ts` identity rows |
123
+ | T7 | Commands surface | `/chapters-link treedx+…` resolve-or-creates the managed repo, stores the token 0600 under the DSH home, records kind+repoId; view/status print provider + head | `link-treedx.test.ts` (5) |
124
+ | T8 | Dead-mirror self-heal | a pre-existing non-empty mirror dir (stale test residue, moved tree, abandoned pool) moves aside as `.stale-<ts>` and the clone proceeds — the old hard refusal made every later sync of an affected workspace a permanent deadlock (the r39 e2e class) | unit `sync.test.ts` self-heal row; `treedx-provider.test.ts` clone row (both providers); e2e boots now run dirty-free |
125
+ | T9 | Body-hash anchor round-trips | the frontmatter anchor hashes EXACTLY what `parseChapterFile` returns as the body (the writer appends `'\n'`; hashing bare `bodyText` made every real chapter look tampered to the enrich guard, which silently refused — host logger). Sanctioned writes preserve body bytes and re-anchor the registry's whole-file hash | unit `enrich-store.test.ts`/`enrich-wire.test.ts`; **the enrich e2e passed 2026-09-23 — the first time the ladder ever wrote through the real UI** |
126
+ | T10 | Host-plane enrichment route | `/chapters-enrich run` resolves a route from the last real tool-bearing session (`noteRoute` persists `enrichment.route`; the host has no live agent) — without it, a fork-only flow could never enrich | `enrich-wire.test.ts` + the passing enrich e2e |
127
+ | T11 | Git-plane auth honesty | a rejected credential (401/403) classifies as `auth`, never `network` — clone/pull/push carry it and the degrade-to-local-only status says `credentials rejected … re-link with /chapters-link`, not the unreachable-ghost (the TreeDX live lesson applied to git) | `git-auth-classification.test.ts` (5) over a REAL smart-HTTP remote with a token gate; classifier unit rows included |
128
+ | T12 | Corpus listing is complete | `paths/list` paginates (limit 100, base64 offset cursor) — `corpusPaths` follows `nextCursor` to the end; a second machine materializes EVERY file past a page boundary | `treedx-sync.test.ts` pagination row (150 files over the paginating stub); the live container (>100 paths) proves it at the real service |
129
+ | T13 | The human entry surface | `/chapters-link` all four modes incl. refusals (no-token non-loopback, junk target), `/chapters-status` renders each fact ONCE (a duplicate-`Provider:` line was caught here), `/chapters-enrich` verbs, legacy workspace-token migration to the DSH home (0600, old file deleted) | `link-git-commands.test.ts` (11), `token-migration.test.ts` (4) |
130
+ | COV | Both coverage ledgers are statement-grade and run | `npm run coverage:ci` — the GATED deterministic ledger (c8 over all unit+integration, `--check-coverage` at 85% statements/branches/functions/lines; enforced in hosted CI, which is GREEN end to end 2026-09-25) — currently **97.98% stmts / 85.61% branch / 93.62% funcs @ 451 tests, zero skips**. `npm run coverage:e2e` — six-project replay under E2E_COVERAGE=1; boots run lib/*.js with REAL source maps (tsconfig `sourceMap`) remapped by c8/v8-to-istanbul; first ledger **80% stmts / 62% branches / 74% funcs** — it corrected the earlier line-range estimate honestly (tools.ts 99% lines → 72% statements). The 2026-09-23 audit then closed most of that gap at the DETERMINISTIC plane: the engine's construction/listeners/degraded-summarize/plot-elicit/finalize-guard paths (`engine-lifecycle.test.ts` — real cordis `Context()`, real base engine, injected llm/tokenMeter fakes: engine.ts 40→91% stmts), every tool wrapper's success/refusal/missing-caller surface (`tools-surface` + `tools-branches` + `tools-deep-branches`), the TreeDX error taxonomy via one-shot stub faults (`treedx-provider-branches`) and parser-shape drift via injected `fetchImpl` (`treedx-parser-branches`), the local-path git transport arms (`gitops-branches`), host-command error arms (`host-command-branches`), and the pure planners (vocabulary/enrich/indexing/search/arrival/archive — `rules-vocab-branches`, `enrich-planner-branches`, `indexing-branches`, `notice-search-rules-branches`, `render-archive-ladder-branches`, `pure-branches`). It also caught one REAL bug on the way: `canonicalizeRemote` carried embedded `user:token@` credentials into the project key (pasting a remote with and without creds forked the pool — now stripped, pinned in `pure-branches.test.ts`). Gotchas pinned in the script header: c8 filters include/exclude BEFORE remapping (`--include=src/**` against lib-path profiles zeroes the report). Neither ledger alone is the claim; the e2e one makes index/render-in-browser paths that no deterministic test can see accountable |
131
+ | H1 | Corpus carries no project instructions | host-injected context (instruction files, runtime snapshot, skill catalog, our own TOC notice — event-level AND `<system-reminder>` spans inside human messages) renders as a countable `⟦omitted:host-injected …⟧` marker, never bytes; conversation survives verbatim and byte-identical | unit `injections.test.ts` (15) + `render.test.ts` rows; integration `corpus-hygiene.test.ts` (archive→sync→remote→pull→search sentinel-absence, and legacy-store-publishes-unchanged anti-migration guard); e2e stays AGENTS-BLIND (tape masking) |
132
+
133
+ ## The Cache Metric, Before You Trust a Number
134
+
135
+ `usage.inputTokens` is the **uncached delta** and `usage.cacheReadTokens` is the cached prefix, so:
136
+
137
+ ```
138
+ totalPrompt = inputTokens + cacheReadTokens
139
+ hitRate = cacheReadTokens / totalPrompt // NOT cacheRead / input
140
+ ```
141
+
142
+ Dividing by the delta yields nonsense like 1721% — which these notes actually did before it was caught.
143
+ For "the parent's cache is undisturbed" (G2), assert **`cacheReadTokens` does not collapse**. Do *not*
144
+ assert that hit percentage stays flat: it falls innocently every time the prompt grows past a cache-block
145
+ boundary while the prefix is fully retained. Measured steady state on this harness: 396,800 cached + 822
146
+ uncached, **99.79%**.
147
+
148
+ ## Engine-Path Checks (Phase 0b delta)
149
+
150
+ For the `ChaptersCompactionEngine` path (in-place compaction, no new session). Rounds 12/13/17 already
151
+ proved the *mechanism* offline (durable records, zero usage, realm dispatch — see
152
+ [spikes/probe/FINDINGS.md](../spikes/probe/FINDINGS.md) § Phase 0b); these are the product-level checks
153
+ that still need a running chapter engine:
154
+
155
+ | # | Check | Passes when |
156
+ |---|---|---|
157
+ | E1 | Zero-inference summarization | the `compaction/summary` event carries **no `usage` field** and no provider call appears in the round (proved with a stub at r12/r13; must re-prove with the real TOC `summarize()`) |
158
+ | E2 | Archive honesty after automatic compaction | every `shadowedSeq` of a landed compaction resolves (via durable log + `sourceEventSeqs`) into a written chapter file whose hash matches the registry |
159
+ | E3 | Header cache across a compaction — **CLOSED by r23 (measured, per-machine)** | cloud: `cacheReadTokens` held at **7,424 on both post-replacement turns** while uncached refill fell 13,658 → ~7,000. Local llama.cpp (r28-era probes): reuses NOTHING across a head replacement (0 cached) but steady-state turns cost only the new content. **Never quote one machine's number for the other.** |
160
+ | E2E-TAPE | Acceptance without the GPU | `test:e2e:replay` serves every model turn from `var/model-tape/<project>` (isolated per boot) (proxy smoke: exact + prefix hit, loud 503 miss with journal). Usage fields ride the tape, so the token meter and compaction thresholds fire at identical steps under replay. Tapes are committed test DATA under `tests/fixtures/model-tape/` (the rest of `var/` stays ignored) — CI replays them via `scripts/ci-replay.sh` with no local model; regenerate via `test:e2e:record` (local GPU) when PRODUCT prompts change. The local model remains the distiller and ultimate witness. STATUS 2026-09-23: pipeline current (escape/whitespace classes, volatile rules incl. HOSTPORT-BLIND + derived-count collapses for `(N est tokens)` and host-injected char counts, tool bytes matched out); SIX-project replay chain (suite, fanout, heavy, arrival, enrich, rules). Doc-edit cost is honest, not free: the delimiter mask keeps AGENTS/skill bytes out of ordinary matching, but the harness's own checkpoint summary embeds the doc as model-visible prose in the compaction scenarios, so `heavy`/`fanout` re-record after an AGENTS.md change (finalize docs, THEN distill). The e2e is the integration witness that the deterministic layers cannot replace — it found two real product bugs (the enrich body-hash anchor mismatch and the missing host-plane route) that had been invisible behind the host logger |
161
+ | E9 | P2 enrichment + vocabulary, two-machine grade | `node --test tests/integration/two-machine-enrich.test.ts` + `--project=enrich` tape replay | **green 2026-09-20** — enriched frontmatter/provenance cross the pool byte-exact; B honors A’s aliases and dedupes its own vocab pass; caught+fixed r37 (modifications invisible to commits). Four-project replay chain green, 0 misses |
162
+ | E2E | The fork BUTTON, browser journey | **green (2026-09-19, 7 specs / three projects — suite@0.75-of-32K + heavy@0.5-floor-off + arrival@0.9-floor-500 — on port-keyed throwaway homes)**: specs CREATE their sessions through the UI ('New session' + one real Local-model turn; turn-complete = the session's OWN `turn/end` in its own log — the global registry count false-passed twice and was retired). Fixture-free by design after the R29-era rows aged out of the 90-session sidebar. The suite also caught the 'Internal Testing Notice' focus trap and the re-link origin bug (see FINDINGS). |
163
+ | E10 | P3 rules lifecycle (record §7) | unit (status resolution, budget refusal, golden absent-notice bytes) + transport (three machines over a real pool) + e2e `rules` project replay | proposed everywhere by default; per-machine core facts; verbatim in the child's OWN log after UI approve; overflow refuses with per-rule numbers — **green 2026-09-20 at unit/transport grade** (three machines over a real pool; exit criterion covered); the `rules` browser scenario parked out of the default chain pending the r39 diagnosis — evidence-preservation first |
164
+ | E2E-EDGE | Oversized turn handled gracefully | **green in the heavy project (captures 5.3–13.2m; run 11: 5 crossings, 27.8m; 6.0m suite pass)**: one browser turn reads 700KB range-by-range and crosses the threshold 5×: every mid-turn compaction commits (provider `dsh-chapters`, zero usage), every shadowed seq is chapter-covered, the pre-compaction marker survives in the transcript, the turn completes, and the follow-up turn answers — fork row reappears after settle (observed). The model's plan erosion under heavy trims is the fork's motivation; FINDINGS records it. |
165
+ | E4 | Pruner composition | with `tool-result-pruner` mounted in the realm, a chapter covering a pruned result still renders the ORIGINAL text (follow `sourceEventSeqs` backwards), plus the model's inline override where requested |
166
+ | E5 | Shrink-floor refusal is loud | a region too small to yield a smaller framed TOC fails with `ManualCompactionError code='summary'` and changes no surface (r12 behavior; keep as regression) |
167
+ | E6 | Automatic timing inherits the host's |
168
+ | E7 | Arrival-time artifacting (architecture amendment) | **green 2026-09-19 (9.3m)**: 373KB book / one read → exactly 1 arrival prune at the tail; model answers buried token + chapter count via 5 `chapters_artifact` calls; ZERO compaction events at production 0.9; blob fingerprint in no `request/*` event; cache table in FINDINGS shows prefix intact (cacheRead monotonically 15.7K→24.5K) |
169
+ | E8 | Plot carriage across in-place checkpoints | forward path live (persona discipline — suiteB oversized run carries `PLOT:` blocks through 7 compactions); render→extract round trip pinned in unit (`carried plot survives the checkpoint round-trip`); elicited fallback unit-pinned (capped input/output, degrades to null) — fires only when the agent writes no note | compaction lands at `agent/pre-step`, bracketed `compaction/start.turn` = open turn number (r13/r17 `turn: 2`); never mid-request, never mid-step |
170
+
171
+ ## Bug Reports
172
+
173
+ Cache and context behaviour are the plugin's core promise, so a report must include:
174
+
175
+ - parent session id, child session id, root session id, store directory, archive ceiling
176
+ - chapter paths written, and any artifact references
177
+ - `usage.cacheReadTokens` for the parent's steps immediately before and after, plus the child's first two
178
+ steps — **and the process generation (which restart) each was taken in**
179
+ - **first-request token counts for both sessions** — the single most diagnostic number, since a silently
180
+ re-seeded child is the most damaging regression
181
+ - the ranges supplied and the refusal text, if a preflight rejected the attempt
182
+
183
+ ## The first real-remote push (manual, one-time per project)
184
+
185
+ The sync loop is integration-tested against a fake driver that models the git
186
+ semantics it relies on; the wire protocol itself is isomorphic-git's (https only
187
+ — isomorphic-git 1.42 registers no `file://` transport, verified against its
188
+ transport registry). The first push against a real remote is therefore a
189
+ **manual, one-time step per project**:
190
+
191
+ 1. Create a **private** empty repo (record §2.1: per-project, user-created) and a
192
+ scoped token for it (fine-grained PAT limited to that repo).
193
+ 2. In a Chapters session: `/chapters-link https://… <token>`.
194
+ The token is stored 0600 at `.dsh-chapters/.git-auth/<projectKey>`.
195
+ 3. The command reports the sync steps; on `push rejected`, re-run
196
+ `/chapters-link` (pull-retry) or wait for the next archive event.
197
+
198
+ If isomorphic-git ever chokes on a specific forge's smart-HTTP, the documented
199
+ fallback is a CLI driver (the `GitDriver` seam in `src/gitops.ts` exists for
200
+ exactly this swap) — the loop's logic is driver-agnostic by design.
@@ -0,0 +1,88 @@
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
+ reserve(key: string, count: number): Promise<number[]>;
21
+ }
22
+ export interface ChapterRecord {
23
+ number: number;
24
+ path: string;
25
+ title: string;
26
+ summary: string;
27
+ startSeq: number;
28
+ endSeq: number;
29
+ /** Index topics (deterministic floor; P2 enrichment may relabel). */
30
+ topics: string[];
31
+ /** user+assistant message count in the chapter (record §7.3 notice line);
32
+ * absent for records written before counts existed — never a lying zero.
33
+ * (`| undefined` for the `exactOptionalPropertyTypes` zod round-trip.) */
34
+ messages?: number | undefined;
35
+ /**
36
+ * The exact surface seqs archived, in surface order. Optional because
37
+ * continuation-path ranges are numeric-contiguous (start/end suffice), but
38
+ * engine-path spans replace across the log, where start/end are only span
39
+ * bounds — the set is the truth about coverage. (`| undefined` so the
40
+ * zod-inferred type satisfies `exactOptionalPropertyTypes` in store.ts.)
41
+ */
42
+ shadowedSeqs?: number[] | undefined;
43
+ /** Body sha256, used to detect on-disk tampering before the text is ever cited. */
44
+ sha256: string;
45
+ estimatedTokens: number;
46
+ artifacts: Array<{
47
+ path: string;
48
+ sha256: string;
49
+ bytes: number;
50
+ }>;
51
+ }
52
+ export interface WriteResult {
53
+ records: ChapterRecord[];
54
+ artifactPathsWritten: string[];
55
+ /** Duplicate titles, retried attempts reusing numbers, or content that failed read-back. */
56
+ warnings: string[];
57
+ }
58
+ /** Filesystem-safe, stable, and never empty. Title is display metadata; the number is the identity. */
59
+ export declare function slugify(title: string): string;
60
+ export declare const chapterFileName: (n: number, title: string) => string;
61
+ /** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
62
+ export declare const attemptKey: (sourceSessionId: string, archiveCeiling: number) => string;
63
+ export interface WriteInput {
64
+ fs: ArchiveFs;
65
+ allocator: NumberAllocator;
66
+ /** Store root, relative to the session workspace so the `read` tool can reach it. */
67
+ storeRoot: string;
68
+ /** Directory key — the root of the ancestry tree, not the per-fork id. One dir per tree. */
69
+ rootSessionId: string;
70
+ chapters: readonly RenderedChapter[];
71
+ /** Reuse a prior attempt's allocation when this is a retry of identical work. */
72
+ attemptId: string;
73
+ }
74
+ export declare function writeArchive(input: WriteInput): Promise<WriteResult>;
75
+ /** Coverage arithmetic: which seqs below the ceiling were NOT archived. Gaps are reported, never silent. */
76
+ export declare function coverage(records: readonly {
77
+ startSeq: number;
78
+ endSeq: number;
79
+ }[], archiveCeiling: number, archiveFloor?: number): {
80
+ covered: number;
81
+ gaps: Array<{
82
+ from: number;
83
+ to: number;
84
+ }>;
85
+ };
86
+ export type VerifyResult = 'ok' | 'modified' | 'missing';
87
+ /** Called before citing a chapter in an index. Detects accidental drift and single-shot injection. */
88
+ export declare function verifyChapter(fs: ArchiveFs, record: ChapterRecord): Promise<VerifyResult>;
package/lib/archive.js ADDED
@@ -0,0 +1,114 @@
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
+ export const chapterFileName = (n, title) => `${String(n).padStart(3, '0')}-${slugify(title)}.md`;
8
+ /** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
9
+ export const attemptKey = (sourceSessionId, archiveCeiling) => `${sourceSessionId}@${archiveCeiling}`;
10
+ /** Join without importing path, so the seam stays portable between ctx.fs and node:fs. */
11
+ const join = (...parts) => parts.filter(Boolean).join('/').replace(/\/{2,}/g, '/');
12
+ export async function writeArchive(input) {
13
+ const { fs, allocator, storeRoot, rootSessionId, chapters } = input;
14
+ if (chapters.length === 0)
15
+ throw new Error('writeArchive called with no chapters');
16
+ const treeDir = join(storeRoot, rootSessionId);
17
+ const warnings = [];
18
+ // 1. Reserve BEFORE touching the filesystem, so a crash cannot re-use a number.
19
+ const numbers = await allocator.reserve(input.attemptId, chapters.length);
20
+ if (numbers.length !== chapters.length) {
21
+ throw new Error(`allocator returned ${numbers.length} numbers for ${chapters.length} chapters`);
22
+ }
23
+ const records = [];
24
+ const artifactPathsWritten = [];
25
+ const seenArtifact = new Map(); // sha256 → path, for subtree dedup
26
+ const seenFile = new Set();
27
+ for (let i = 0; i < chapters.length; i++) {
28
+ const chapter = chapters[i];
29
+ const number = numbers[i];
30
+ let fileName = chapterFileName(number, chapter.range.title);
31
+ if (seenFile.has(fileName)) {
32
+ // Same title, same number prefix — disambiguate rather than let one chapter overwrite another.
33
+ fileName = fileName.replace(/\.md$/, `-${number}.md`);
34
+ warnings.push(`title collision resolved for "${chapter.range.title}" → ${fileName}`);
35
+ }
36
+ seenFile.add(fileName);
37
+ const chapterPath = join(treeDir, 'chapters', fileName);
38
+ // 2. Artifacts first: the chapter body cites these paths, so they must exist before it does.
39
+ for (const artifact of chapter.artifacts) {
40
+ const existing = seenArtifact.get(artifact.sha256);
41
+ if (existing !== undefined)
42
+ continue; // identical result in this batch
43
+ if (await fs.exists(join(treeDir, artifact.path))) { // identical result from an earlier fork
44
+ seenArtifact.set(artifact.sha256, artifact.path);
45
+ continue;
46
+ }
47
+ await fs.write(join(treeDir, artifact.path), artifact.content);
48
+ seenArtifact.set(artifact.sha256, artifact.path);
49
+ artifactPathsWritten.push(join(treeDir, artifact.path));
50
+ }
51
+ // 3. The chapter itself.
52
+ await fs.write(chapterPath, chapter.markdown);
53
+ // 4. Read-back verification. A file that does not come back byte-identical is not citable.
54
+ const readBack = await fs.read(chapterPath);
55
+ if (readBack === undefined) {
56
+ warnings.push(`read-back failed (missing): ${chapterPath}`);
57
+ continue;
58
+ }
59
+ // Hash the whole file, so verifyChapter() can re-check it with no knowledge of frontmatter layout.
60
+ const bodyHash = sha256(chapter.markdown);
61
+ if (readBack !== chapter.markdown) {
62
+ warnings.push(`read-back mismatch, refusing to cite: ${chapterPath}`);
63
+ continue;
64
+ }
65
+ if (chapter.stats.unrenderedSeqs.length > 0) {
66
+ warnings.push(`${chapterPath}: ${chapter.stats.unrenderedSeqs.length} event(s) rendered no output (${chapter.stats.unrenderedSeqs.join(', ')})`);
67
+ }
68
+ if (chapter.stats.overTarget) {
69
+ warnings.push(`${chapterPath}: ${chapter.stats.estimatedTokens} tokens over chapterTokenTarget — split at the next boundary, do not clip`);
70
+ }
71
+ records.push({
72
+ number,
73
+ path: chapterPath,
74
+ title: chapter.range.title,
75
+ summary: chapter.range.summary,
76
+ startSeq: chapter.range.startSeq,
77
+ endSeq: chapter.range.endSeq,
78
+ topics: chapter.topics,
79
+ messages: chapter.stats.messages,
80
+ sha256: bodyHash,
81
+ estimatedTokens: chapter.stats.estimatedTokens,
82
+ artifacts: chapter.artifacts.map((a) => ({ path: join(treeDir, a.path), sha256: a.sha256, bytes: a.bytes })),
83
+ });
84
+ }
85
+ return { records, artifactPathsWritten, warnings };
86
+ }
87
+ /** Coverage arithmetic: which seqs below the ceiling were NOT archived. Gaps are reported, never silent. */
88
+ export function coverage(records, archiveCeiling, archiveFloor = 0) {
89
+ const sorted = [...records].sort((a, b) => a.startSeq - b.startSeq);
90
+ const gaps = [];
91
+ // Start at the floor, not at the first chapter: a set that skips the opening of the conversation is
92
+ // exactly the silent-history-loss this function exists to catch.
93
+ let cursor = archiveFloor;
94
+ let covered = 0;
95
+ for (const r of sorted) {
96
+ if (r.startSeq > cursor)
97
+ gaps.push({ from: cursor, to: r.startSeq - 1 });
98
+ cursor = Math.max(cursor, r.endSeq + 1);
99
+ covered += r.endSeq - r.startSeq + 1;
100
+ }
101
+ if (cursor <= archiveCeiling)
102
+ gaps.push({ from: cursor, to: archiveCeiling });
103
+ return { covered, gaps };
104
+ }
105
+ /** Called before citing a chapter in an index. Detects accidental drift and single-shot injection. */
106
+ export async function verifyChapter(fs, record) {
107
+ const text = await fs.read(record.path);
108
+ if (text === undefined)
109
+ return 'missing';
110
+ if (sha256(text) !== record.sha256)
111
+ return 'modified';
112
+ return 'ok';
113
+ }
114
+ //# 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>;