@isaacriehm/cairn-state 0.22.5

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/alignment-pending.d.ts +28 -0
  5. package/dist/alignment-pending.js +83 -0
  6. package/dist/alignment-pending.js.map +1 -0
  7. package/dist/anchor-map.d.ts +14 -0
  8. package/dist/anchor-map.js +56 -0
  9. package/dist/anchor-map.js.map +1 -0
  10. package/dist/archive.d.ts +48 -0
  11. package/dist/archive.js +96 -0
  12. package/dist/archive.js.map +1 -0
  13. package/dist/cache.d.ts +48 -0
  14. package/dist/cache.js +241 -0
  15. package/dist/cache.js.map +1 -0
  16. package/dist/component-registry.d.ts +93 -0
  17. package/dist/component-registry.js +0 -0
  18. package/dist/component-registry.js.map +1 -0
  19. package/dist/components.d.ts +192 -0
  20. package/dist/components.js +603 -0
  21. package/dist/components.js.map +1 -0
  22. package/dist/config.d.ts +9 -0
  23. package/dist/config.js +26 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/drift.d.ts +8 -0
  26. package/dist/drift.js +23 -0
  27. package/dist/drift.js.map +1 -0
  28. package/dist/file-candidates-map.d.ts +23 -0
  29. package/dist/file-candidates-map.js +76 -0
  30. package/dist/file-candidates-map.js.map +1 -0
  31. package/dist/frontmatter.d.ts +32 -0
  32. package/dist/frontmatter.js +77 -0
  33. package/dist/frontmatter.js.map +1 -0
  34. package/dist/fs.d.ts +36 -0
  35. package/dist/fs.js +47 -0
  36. package/dist/fs.js.map +1 -0
  37. package/dist/glob.d.ts +10 -0
  38. package/dist/glob.js +46 -0
  39. package/dist/glob.js.map +1 -0
  40. package/dist/home.d.ts +69 -0
  41. package/dist/home.js +168 -0
  42. package/dist/home.js.map +1 -0
  43. package/dist/index.d.ts +29 -0
  44. package/dist/index.js +30 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/languages.d.ts +113 -0
  47. package/dist/languages.js +512 -0
  48. package/dist/languages.js.map +1 -0
  49. package/dist/ledgers.d.ts +14 -0
  50. package/dist/ledgers.js +105 -0
  51. package/dist/ledgers.js.map +1 -0
  52. package/dist/logger.d.ts +13 -0
  53. package/dist/logger.js +17 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/manifest.d.ts +10 -0
  56. package/dist/manifest.js +84 -0
  57. package/dist/manifest.js.map +1 -0
  58. package/dist/missions.d.ts +119 -0
  59. package/dist/missions.js +414 -0
  60. package/dist/missions.js.map +1 -0
  61. package/dist/paths.d.ts +117 -0
  62. package/dist/paths.js +241 -0
  63. package/dist/paths.js.map +1 -0
  64. package/dist/quality-grades.d.ts +11 -0
  65. package/dist/quality-grades.js +100 -0
  66. package/dist/quality-grades.js.map +1 -0
  67. package/dist/rejected.d.ts +42 -0
  68. package/dist/rejected.js +100 -0
  69. package/dist/rejected.js.map +1 -0
  70. package/dist/schemas.d.ts +789 -0
  71. package/dist/schemas.js +506 -0
  72. package/dist/schemas.js.map +1 -0
  73. package/dist/scope-index.d.ts +96 -0
  74. package/dist/scope-index.js +299 -0
  75. package/dist/scope-index.js.map +1 -0
  76. package/dist/slug.d.ts +81 -0
  77. package/dist/slug.js +138 -0
  78. package/dist/slug.js.map +1 -0
  79. package/dist/sot-bindings.d.ts +14 -0
  80. package/dist/sot-bindings.js +79 -0
  81. package/dist/sot-bindings.js.map +1 -0
  82. package/dist/sot-cache.d.ts +18 -0
  83. package/dist/sot-cache.js +62 -0
  84. package/dist/sot-cache.js.map +1 -0
  85. package/dist/text.d.ts +27 -0
  86. package/dist/text.js +63 -0
  87. package/dist/text.js.map +1 -0
  88. package/dist/topic-index.d.ts +27 -0
  89. package/dist/topic-index.js +82 -0
  90. package/dist/topic-index.js.map +1 -0
  91. package/dist/walk.d.ts +7 -0
  92. package/dist/walk.js +34 -0
  93. package/dist/walk.js.map +1 -0
  94. package/package.json +35 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Scope index — forward map from every file path in the repo to the decisions
3
+ * and invariants that apply to that file.
4
+ *
5
+ * Built at init by the Tier-2 mapper LLM, maintained by the GC sweep + the
6
+ * MCP record-decision tool when scope edits land. Read by the read-enricher
7
+ * / write-guardian hooks (via cached accessor in
8
+ * `hooks/post-tool-use/ledger-cache.ts`) and by the GC scope-coverage pass.
9
+ *
10
+ * Spec: docs/FILESYSTEM_LAYOUT.md §2.1.
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { cairnDir, isGhost } from "./home.js";
16
+ import { writeFileSafe } from "./fs.js";
17
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
18
+ import { getLogger } from "./logger.js";
19
+ const DEC_ID_RE = /\bDEC-[0-9a-f]{7,}\b/;
20
+ const INV_ID_RE = /\bINV-[0-9a-f]{7,}\b/;
21
+ /**
22
+ * Mapper LLMs occasionally emit ledger-entry PROSE ("HTTP layer is the only
23
+ * public surface…") into `decisions[]` / `invariants[]` instead of bare IDs,
24
+ * because the user prompt lists them as `${id} — ${title}` and the JSON-mode
25
+ * schema only constrains the type to `string`. This coercer extracts the
26
+ * first ID-shaped token from each string and silently drops anything that
27
+ * doesn't match — IDs only, deduplicated, order preserved.
28
+ */
29
+ export function coerceDecisionIds(raw) {
30
+ return coerceIds(raw, DEC_ID_RE);
31
+ }
32
+ export function coerceInvariantIds(raw) {
33
+ return coerceIds(raw, INV_ID_RE);
34
+ }
35
+ function coerceIds(raw, re) {
36
+ const out = [];
37
+ const seen = new Set();
38
+ for (const s of raw) {
39
+ if (typeof s !== "string")
40
+ continue;
41
+ const m = s.match(re);
42
+ if (m === null)
43
+ continue;
44
+ if (seen.has(m[0]))
45
+ continue;
46
+ seen.add(m[0]);
47
+ out.push(m[0]);
48
+ }
49
+ return out;
50
+ }
51
+ export function scopeIndexPath(repoRoot) {
52
+ return cairnDir(repoRoot, "ground", "scope-index.yaml");
53
+ }
54
+ export function readScopeIndex(repoRoot) {
55
+ const path = scopeIndexPath(repoRoot);
56
+ if (!existsSync(path))
57
+ return null;
58
+ let parsed;
59
+ try {
60
+ parsed = parseYaml(readFileSync(path, "utf8"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (typeof parsed !== "object" || parsed === null)
66
+ return null;
67
+ const generated = typeof parsed["generated"] === "string"
68
+ ? parsed["generated"]
69
+ : new Date().toISOString();
70
+ const filesRaw = parsed["files"];
71
+ const files = {};
72
+ if (typeof filesRaw === "object" && filesRaw !== null) {
73
+ for (const [k, v] of Object.entries(filesRaw)) {
74
+ if (typeof v !== "object" || v === null)
75
+ continue;
76
+ const e = v;
77
+ const decisions = Array.isArray(e["decisions"])
78
+ ? e["decisions"].filter((x) => typeof x === "string")
79
+ : [];
80
+ const invariants = Array.isArray(e["invariants"])
81
+ ? e["invariants"].filter((x) => typeof x === "string")
82
+ : [];
83
+ const entry = { decisions, invariants };
84
+ if (e["unscoped"] === true)
85
+ entry.unscoped = true;
86
+ files[k] = entry;
87
+ }
88
+ }
89
+ return { generated, files };
90
+ }
91
+ export function lookupScope(index, repoRelativePath) {
92
+ const entry = index.files[repoRelativePath];
93
+ if (entry === undefined)
94
+ return null;
95
+ return entry;
96
+ }
97
+ export function writeScopeIndex(repoRoot, index) {
98
+ const path = scopeIndexPath(repoRoot);
99
+ writeFileSafe(path, stringifyYaml(index));
100
+ }
101
+ const log = getLogger();
102
+ /* -------------------------------------------------------------------------- */
103
+ /* Deterministic citation rescan */
104
+ /* -------------------------------------------------------------------------- */
105
+ const CITATION_RE = /§(?:INV|DEC)-\d{4,}/g;
106
+ const RESCAN_SOURCE_EXT_RE = /\.(?:ts|tsx|cts|mts|js|jsx|cjs|mjs|py|rb|go|rs|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sql|sh|bash)$/i;
107
+ const RESCAN_SKIP_DIR_RE = /(?:^|\/)(?:node_modules|dist|build|out|\.next|\.turbo|\.cache|coverage|\.cairn|\.archive|\.git)(?:\/|$)/;
108
+ /**
109
+ * Deterministic regex sweep — walk every git-tracked source file, parse
110
+ * `§INV-<hash>` / `§DEC-<hash>` cite tokens, and sync the scope-index so the
111
+ * in-scope tools never lag behind source-cite reality. No LLM, no
112
+ * incremental tracking complexity. Cheap enough to run on every
113
+ * SessionStart (~100ms on 50k files).
114
+ *
115
+ * Honors `unscoped: true` entries — never touches them. Skips files with
116
+ * no citations and no prior entry to avoid polluting the index. When a
117
+ * file's cite set differs from its scope-index entry (added, removed, or
118
+ * stale ids), the entry is rewritten with the deterministically-sorted
119
+ * current set.
120
+ */
121
+ export function rescanScopeIndex(repoRoot) {
122
+ // Ghost: scope-index is the SoT (written at Layer A emit), NOT a cache
123
+ // derived from in-source cites. There are no cites to rescan — and a rescan
124
+ // would rewrite every bound file's entry to the empty cite-set, wiping the
125
+ // SoT. No-op so the bindings the GC + recall depend on survive SessionStart
126
+ // and `cairn join`. (Maintenance happens via anchor-map re-anchoring, §3.6.)
127
+ if (isGhost(repoRoot)) {
128
+ return { filesScanned: 0, entriesAdded: 0, entriesUpdated: 0, entriesUnchanged: 0, dirty: false };
129
+ }
130
+ const sourceFiles = listGitTrackedSourceFiles(repoRoot);
131
+ const existing = readScopeIndex(repoRoot) ?? {
132
+ generated: new Date().toISOString(),
133
+ files: {},
134
+ };
135
+ let entriesAdded = 0;
136
+ let entriesUpdated = 0;
137
+ let entriesUnchanged = 0;
138
+ let dirty = false;
139
+ for (const rel of sourceFiles) {
140
+ const prior = existing.files[rel];
141
+ if (prior?.unscoped === true)
142
+ continue;
143
+ const found = scanFileCitations(join(repoRoot, rel));
144
+ if (found === null)
145
+ continue;
146
+ if (found.decisions.length === 0 &&
147
+ found.invariants.length === 0 &&
148
+ prior === undefined) {
149
+ continue;
150
+ }
151
+ if (prior === undefined) {
152
+ existing.files[rel] = {
153
+ decisions: found.decisions,
154
+ invariants: found.invariants,
155
+ };
156
+ entriesAdded += 1;
157
+ dirty = true;
158
+ continue;
159
+ }
160
+ const sameDecs = prior.decisions.length === found.decisions.length &&
161
+ prior.decisions.every((d, i) => d === found.decisions[i]);
162
+ const sameInvs = prior.invariants.length === found.invariants.length &&
163
+ prior.invariants.every((v, i) => v === found.invariants[i]);
164
+ if (sameDecs && sameInvs) {
165
+ entriesUnchanged += 1;
166
+ continue;
167
+ }
168
+ existing.files[rel] = {
169
+ decisions: found.decisions,
170
+ invariants: found.invariants,
171
+ };
172
+ entriesUpdated += 1;
173
+ dirty = true;
174
+ }
175
+ if (dirty) {
176
+ writeScopeIndex(repoRoot, {
177
+ generated: new Date().toISOString(),
178
+ files: existing.files,
179
+ });
180
+ }
181
+ return {
182
+ filesScanned: sourceFiles.length,
183
+ entriesAdded,
184
+ entriesUpdated,
185
+ entriesUnchanged,
186
+ dirty,
187
+ };
188
+ }
189
+ function scanFileCitations(absPath) {
190
+ let body;
191
+ try {
192
+ body = readFileSync(absPath, "utf8");
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ return parseCitations(body);
198
+ }
199
+ function parseCitations(body) {
200
+ const decs = new Set();
201
+ const invs = new Set();
202
+ for (const m of body.matchAll(CITATION_RE)) {
203
+ const tok = m[0].slice(1);
204
+ if (tok.startsWith("DEC-"))
205
+ decs.add(tok);
206
+ else if (tok.startsWith("INV-"))
207
+ invs.add(tok);
208
+ }
209
+ return {
210
+ decisions: [...decs].sort(),
211
+ invariants: [...invs].sort(),
212
+ };
213
+ }
214
+ /**
215
+ * Single-file scope-index sync from in-memory content. Called by the
216
+ * PostToolUse(Write/Edit) hook so an agent's writes don't leave the
217
+ * scope-index stale until the next SessionStart rescan. Same regex
218
+ * parser as `rescanScopeIndex`, but bounded to one file — O(1) cost,
219
+ * no walker.
220
+ *
221
+ * Honors `unscoped: true` entries (skips them). Honors the same skip
222
+ * rule the rescan does: file with no citations and no prior entry is
223
+ * a no-op so we don't pollute the index with empty rows.
224
+ */
225
+ export function syncFileScopeFromContent(repoRoot, repoRelPath, content) {
226
+ const found = parseCitations(content);
227
+ const existing = readScopeIndex(repoRoot) ?? {
228
+ generated: new Date().toISOString(),
229
+ files: {},
230
+ };
231
+ const prior = existing.files[repoRelPath];
232
+ if (prior?.unscoped === true)
233
+ return { dirty: false };
234
+ if (found.decisions.length === 0 &&
235
+ found.invariants.length === 0 &&
236
+ prior === undefined) {
237
+ return { dirty: false };
238
+ }
239
+ if (prior !== undefined) {
240
+ const sameDecs = prior.decisions.length === found.decisions.length &&
241
+ prior.decisions.every((d, i) => d === found.decisions[i]);
242
+ const sameInvs = prior.invariants.length === found.invariants.length &&
243
+ prior.invariants.every((v, i) => v === found.invariants[i]);
244
+ if (sameDecs && sameInvs)
245
+ return { dirty: false };
246
+ }
247
+ existing.files[repoRelPath] = {
248
+ decisions: found.decisions,
249
+ invariants: found.invariants,
250
+ };
251
+ writeScopeIndex(repoRoot, {
252
+ generated: new Date().toISOString(),
253
+ files: existing.files,
254
+ });
255
+ return { dirty: true };
256
+ }
257
+ function listGitTrackedSourceFiles(repoRoot) {
258
+ let out;
259
+ try {
260
+ out = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd: repoRoot, maxBuffer: 100 * 1024 * 1024 }).toString("utf8");
261
+ }
262
+ catch {
263
+ return [];
264
+ }
265
+ const acc = [];
266
+ for (const path of out.split("\0")) {
267
+ if (path.length === 0)
268
+ continue;
269
+ if (RESCAN_SKIP_DIR_RE.test(path))
270
+ continue;
271
+ if (!RESCAN_SOURCE_EXT_RE.test(path))
272
+ continue;
273
+ acc.push(path);
274
+ }
275
+ return acc;
276
+ }
277
+ /**
278
+ * `cairn scope rebuild` — full deterministic resync of `.cairn/ground/
279
+ * scope-index.yaml` from current source-cite reality. Used to be a
280
+ * Sonnet call (`runMapper`-style) that re-classified files via the
281
+ * Tier-2 mapper, but classification was always the wrong abstraction
282
+ * — bare-symbol citations in source are the canonical source of truth,
283
+ * `rescanScopeIndex` parses them deterministically, and the result
284
+ * matches what the read-enricher legend actually shows.
285
+ *
286
+ * No LLM. No tokens. No mapper. Just a regex sweep over git-tracked
287
+ * source files and an atomic write.
288
+ */
289
+ export async function rebuildScopeIndex(opts) {
290
+ log.info({ repo_root: opts.repoRoot }, "scope rebuild — deterministic rescan");
291
+ const startedAt = Date.now();
292
+ const result = rescanScopeIndex(opts.repoRoot);
293
+ return {
294
+ path: scopeIndexPath(opts.repoRoot),
295
+ filesClassified: result.entriesAdded + result.entriesUpdated + result.entriesUnchanged,
296
+ durationMs: Date.now() - startedAt,
297
+ };
298
+ }
299
+ //# sourceMappingURL=scope-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-index.js","sourceRoot":"","sources":["../src/scope-index.ts"],"names":[],"mappings":"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"}
package/dist/slug.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Normalize a prose block: strip markdown ornaments + collapse whitespace.
3
+ * Output is the canonical form fed into both content-fingerprint slug
4
+ * generation and content-hash calculation. Identical normalized output
5
+ * → identical slug, even if the source markdown formatting differs.
6
+ */
7
+ export declare function normalizeBlock(input: string): string;
8
+ /**
9
+ * Compute the 12-char content-fingerprint slug for a prose block. Uses
10
+ * sha256 over the normalized body, hex-encoded, sliced to 12 chars.
11
+ *
12
+ * 12 chars → 48 bits, ~280 trillion buckets. Birthday collision becomes
13
+ * meaningful around 16 million distinct topics; real repos are nowhere
14
+ * close to that.
15
+ */
16
+ export declare function topicSlug(input: string): string;
17
+ /**
18
+ * Compute the full sha256 hex of a prose body (no normalization). Used
19
+ * for sot_content_hash drift detection — the hash must change when the
20
+ * operator edits the source paragraph by even one character so the
21
+ * doc-drift sensor can fire.
22
+ */
23
+ export declare function bodyContentHash(body: string): string;
24
+ /**
25
+ * Derive a stable DEC id from canonical inputs. Plan §3.2.1: id =
26
+ * "DEC-" + sha256(JSON.stringify({sot_path, title, capture_source})).slice(0, 7).
27
+ *
28
+ * Body is intentionally NOT in the hash input — doc-drift refresh
29
+ * updates body but keeps the id stable so every §DEC-<hash> token in
30
+ * source files survives the refresh.
31
+ */
32
+ export declare function deriveDecId(input: {
33
+ sot_path: string;
34
+ title: string;
35
+ capture_source: string;
36
+ }): string;
37
+ export declare function deriveInvId(input: {
38
+ sot_path: string;
39
+ title: string;
40
+ capture_source: string;
41
+ }): string;
42
+ /**
43
+ * Ledger-DEC id derivation for source-comment captures (plan §5.3). The
44
+ * generic `deriveDecId` keys on `(sot_path, title, capture_source)`, which
45
+ * collapses for ledger entries because every source-comment DEC shares the
46
+ * literal `sot_path: "ledger"`. Title alone is not unique — two essay
47
+ * comments starting with the same line would collide. Source location is
48
+ * stable post-strip-replace, so `(source_file, source_offset, capture_source)`
49
+ * is the per-fact unique input.
50
+ */
51
+ export declare function deriveLedgerDecId(input: {
52
+ source_file: string;
53
+ source_offset: number;
54
+ capture_source: string;
55
+ }): string;
56
+ export declare function deriveLedgerInvId(input: {
57
+ source_file: string;
58
+ source_offset: number;
59
+ capture_source: string;
60
+ }): string;
61
+ /**
62
+ * Convert an arbitrary title into a kebab-case slug suitable for a
63
+ * mission id (`MIS-<slug>-<hash7>`). Lowercased ASCII; non-alnum
64
+ * collapses to single hyphen; trims leading/trailing hyphens; capped
65
+ * at 32 chars to keep mission ids inside the 40-char statusline budget
66
+ * after the `MIS-` prefix + `-<hash7>` suffix.
67
+ */
68
+ export declare function titleToSlug(title: string, maxLen?: number): string;
69
+ /**
70
+ * Derive a stable mission id from canonical inputs. Format:
71
+ * `MIS-<slug>-<hash7>` mirroring TSK-/DEC-/INV- conventions.
72
+ *
73
+ * Hash inputs include `started_at` so re-running `mission start` on the
74
+ * same spec doc minutes later produces a fresh id (no collision with
75
+ * the prior mission's archived dirs).
76
+ */
77
+ export declare function deriveMissionId(input: {
78
+ title: string;
79
+ spec_path: string;
80
+ started_at: string;
81
+ }): string;
package/dist/slug.js ADDED
@@ -0,0 +1,138 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Content-fingerprint helpers shared by topic-index, anchor-map, sot-cache,
4
+ * and the Layer A alignment hook.
5
+ *
6
+ * The content slug is sha256-derived from the normalized prose body so two
7
+ * identical blocks (in any source) produce the same slug. Different content
8
+ * → different slug, regardless of leading words. This avoids the "we use
9
+ * postgres for X" vs "we use postgres for Y" collision that a leading-word
10
+ * slugger would suffer.
11
+ */
12
+ const NORMALIZE_PATTERNS = [
13
+ { pattern: /```[\s\S]*?```/g, replacement: " " },
14
+ { pattern: /`[^`]+`/g, replacement: " " },
15
+ { pattern: /\[([^\]]+)\]\([^)]*\)/g, replacement: "$1" },
16
+ { pattern: /^#{1,6}\s+/gm, replacement: "" },
17
+ { pattern: /^[\s>*\-+]+/gm, replacement: "" },
18
+ { pattern: /\s+/g, replacement: " " },
19
+ ];
20
+ /**
21
+ * Normalize a prose block: strip markdown ornaments + collapse whitespace.
22
+ * Output is the canonical form fed into both content-fingerprint slug
23
+ * generation and content-hash calculation. Identical normalized output
24
+ * → identical slug, even if the source markdown formatting differs.
25
+ */
26
+ export function normalizeBlock(input) {
27
+ let s = input;
28
+ for (const { pattern, replacement } of NORMALIZE_PATTERNS) {
29
+ s = s.replace(pattern, replacement);
30
+ }
31
+ return s.trim().toLowerCase();
32
+ }
33
+ /**
34
+ * Compute the 12-char content-fingerprint slug for a prose block. Uses
35
+ * sha256 over the normalized body, hex-encoded, sliced to 12 chars.
36
+ *
37
+ * 12 chars → 48 bits, ~280 trillion buckets. Birthday collision becomes
38
+ * meaningful around 16 million distinct topics; real repos are nowhere
39
+ * close to that.
40
+ */
41
+ export function topicSlug(input) {
42
+ const normalized = normalizeBlock(input);
43
+ return createHash("sha256").update(normalized, "utf8").digest("hex").slice(0, 12);
44
+ }
45
+ /**
46
+ * Compute the full sha256 hex of a prose body (no normalization). Used
47
+ * for sot_content_hash drift detection — the hash must change when the
48
+ * operator edits the source paragraph by even one character so the
49
+ * doc-drift sensor can fire.
50
+ */
51
+ export function bodyContentHash(body) {
52
+ return createHash("sha256").update(body, "utf8").digest("hex");
53
+ }
54
+ /**
55
+ * Derive a stable DEC id from canonical inputs. Plan §3.2.1: id =
56
+ * "DEC-" + sha256(JSON.stringify({sot_path, title, capture_source})).slice(0, 7).
57
+ *
58
+ * Body is intentionally NOT in the hash input — doc-drift refresh
59
+ * updates body but keeps the id stable so every §DEC-<hash> token in
60
+ * source files survives the refresh.
61
+ */
62
+ export function deriveDecId(input) {
63
+ const json = JSON.stringify({
64
+ sot_path: input.sot_path,
65
+ title: input.title,
66
+ capture_source: input.capture_source,
67
+ });
68
+ return `DEC-${createHash("sha256").update(json, "utf8").digest("hex").slice(0, 7)}`;
69
+ }
70
+ export function deriveInvId(input) {
71
+ const json = JSON.stringify({
72
+ sot_path: input.sot_path,
73
+ title: input.title,
74
+ capture_source: input.capture_source,
75
+ });
76
+ return `INV-${createHash("sha256").update(json, "utf8").digest("hex").slice(0, 7)}`;
77
+ }
78
+ /**
79
+ * Ledger-DEC id derivation for source-comment captures (plan §5.3). The
80
+ * generic `deriveDecId` keys on `(sot_path, title, capture_source)`, which
81
+ * collapses for ledger entries because every source-comment DEC shares the
82
+ * literal `sot_path: "ledger"`. Title alone is not unique — two essay
83
+ * comments starting with the same line would collide. Source location is
84
+ * stable post-strip-replace, so `(source_file, source_offset, capture_source)`
85
+ * is the per-fact unique input.
86
+ */
87
+ export function deriveLedgerDecId(input) {
88
+ const json = JSON.stringify({
89
+ source_file: input.source_file,
90
+ source_offset: input.source_offset,
91
+ capture_source: input.capture_source,
92
+ });
93
+ return `DEC-${createHash("sha256").update(json, "utf8").digest("hex").slice(0, 7)}`;
94
+ }
95
+ export function deriveLedgerInvId(input) {
96
+ const json = JSON.stringify({
97
+ source_file: input.source_file,
98
+ source_offset: input.source_offset,
99
+ capture_source: input.capture_source,
100
+ });
101
+ return `INV-${createHash("sha256").update(json, "utf8").digest("hex").slice(0, 7)}`;
102
+ }
103
+ /**
104
+ * Convert an arbitrary title into a kebab-case slug suitable for a
105
+ * mission id (`MIS-<slug>-<hash7>`). Lowercased ASCII; non-alnum
106
+ * collapses to single hyphen; trims leading/trailing hyphens; capped
107
+ * at 32 chars to keep mission ids inside the 40-char statusline budget
108
+ * after the `MIS-` prefix + `-<hash7>` suffix.
109
+ */
110
+ export function titleToSlug(title, maxLen = 32) {
111
+ const slug = title
112
+ .toLowerCase()
113
+ .normalize("NFKD")
114
+ .replace(/[^a-z0-9]+/g, "-")
115
+ .replace(/^-+|-+$/g, "")
116
+ .slice(0, maxLen)
117
+ .replace(/-+$/g, "");
118
+ return slug.length > 0 ? slug : "mission";
119
+ }
120
+ /**
121
+ * Derive a stable mission id from canonical inputs. Format:
122
+ * `MIS-<slug>-<hash7>` mirroring TSK-/DEC-/INV- conventions.
123
+ *
124
+ * Hash inputs include `started_at` so re-running `mission start` on the
125
+ * same spec doc minutes later produces a fresh id (no collision with
126
+ * the prior mission's archived dirs).
127
+ */
128
+ export function deriveMissionId(input) {
129
+ const slug = titleToSlug(input.title);
130
+ const json = JSON.stringify({
131
+ title: input.title,
132
+ spec_path: input.spec_path,
133
+ started_at: input.started_at,
134
+ });
135
+ const hash = createHash("sha256").update(json, "utf8").digest("hex").slice(0, 7);
136
+ return `MIS-${slug}-${hash}`;
137
+ }
138
+ //# sourceMappingURL=slug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { SotBindings } from "./schemas.js";
2
+ /**
3
+ * Sot-bindings is the lookup table that lens + sensors + the alignment
4
+ * hook all consult to resolve a §DEC-<hash> token to its canonical
5
+ * source. The forward index is one DEC → one path. The reverse index is
6
+ * one path → many DECs (supersedes chains share their sot_path).
7
+ */
8
+ export declare function emptySotBindings(): SotBindings;
9
+ export declare function readSotBindings(repoRoot: string): SotBindings;
10
+ export declare function writeSotBindings(repoRoot: string, bindings: SotBindings): string;
11
+ export declare function bindDec(bindings: SotBindings, decId: string, sotPath: string): SotBindings;
12
+ export declare function unbindDec(bindings: SotBindings, decId: string): SotBindings;
13
+ export declare function decsForPath(bindings: SotBindings, sotPath: string): string[];
14
+ export declare function pathForDec(bindings: SotBindings, decId: string): string | null;
@@ -0,0 +1,79 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { writeFileSafe } from "./fs.js";
3
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
+ import { getLogger } from "./logger.js";
5
+ import { sotBindingsPath } from "./paths.js";
6
+ import { SotBindings } from "./schemas.js";
7
+ const log = getLogger();
8
+ /**
9
+ * Sot-bindings is the lookup table that lens + sensors + the alignment
10
+ * hook all consult to resolve a §DEC-<hash> token to its canonical
11
+ * source. The forward index is one DEC → one path. The reverse index is
12
+ * one path → many DECs (supersedes chains share their sot_path).
13
+ */
14
+ export function emptySotBindings() {
15
+ return {
16
+ version: 1,
17
+ generated: new Date().toISOString(),
18
+ forward: {},
19
+ reverse: {},
20
+ };
21
+ }
22
+ export function readSotBindings(repoRoot) {
23
+ const path = sotBindingsPath(repoRoot);
24
+ if (!existsSync(path))
25
+ return emptySotBindings();
26
+ try {
27
+ const raw = readFileSync(path, "utf8");
28
+ const parsed = SotBindings.safeParse(parseYaml(raw));
29
+ if (!parsed.success) {
30
+ log.warn({ path, error: parsed.error.message }, "sot-bindings invalid; treating as empty");
31
+ return emptySotBindings();
32
+ }
33
+ return parsed.data;
34
+ }
35
+ catch (err) {
36
+ log.warn({ path, err }, "sot-bindings read failed; treating as empty");
37
+ return emptySotBindings();
38
+ }
39
+ }
40
+ export function writeSotBindings(repoRoot, bindings) {
41
+ const path = sotBindingsPath(repoRoot);
42
+ const next = { ...bindings, generated: new Date().toISOString() };
43
+ writeFileSafe(path, stringifyYaml(next));
44
+ log.debug({ path, decs: Object.keys(next.forward).length }, "wrote sot-bindings");
45
+ return path;
46
+ }
47
+ export function bindDec(bindings, decId, sotPath) {
48
+ const forward = { ...bindings.forward, [decId]: sotPath };
49
+ const reverse = { ...bindings.reverse };
50
+ const existing = reverse[sotPath] ?? [];
51
+ if (!existing.includes(decId)) {
52
+ reverse[sotPath] = [...existing, decId];
53
+ }
54
+ return { ...bindings, forward, reverse };
55
+ }
56
+ export function unbindDec(bindings, decId) {
57
+ const sotPath = bindings.forward[decId];
58
+ if (sotPath === undefined)
59
+ return bindings;
60
+ const forward = { ...bindings.forward };
61
+ delete forward[decId];
62
+ const reverse = { ...bindings.reverse };
63
+ const list = reverse[sotPath] ?? [];
64
+ const filtered = list.filter((id) => id !== decId);
65
+ if (filtered.length === 0) {
66
+ delete reverse[sotPath];
67
+ }
68
+ else {
69
+ reverse[sotPath] = filtered;
70
+ }
71
+ return { ...bindings, forward, reverse };
72
+ }
73
+ export function decsForPath(bindings, sotPath) {
74
+ return bindings.reverse[sotPath] ?? [];
75
+ }
76
+ export function pathForDec(bindings, decId) {
77
+ return bindings.forward[decId] ?? null;
78
+ }
79
+ //# sourceMappingURL=sot-bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sot-bindings.js","sourceRoot":"","sources":["../src/sot-bindings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { SotCache, type SotCacheEntry } from "./schemas.js";
2
+ /**
3
+ * Sot-cache holds pre-tokenized DEC body content for the Layer A Jaccard
4
+ * pre-filter. Rebuilt at SessionStart, incremental on PostToolUse Write
5
+ * events that touch a DEC body or sot-path file. Mtime-keyed so a stale
6
+ * entry is detected without re-tokenizing every body on every Write.
7
+ */
8
+ export declare function emptySotCache(): SotCache;
9
+ export declare function readSotCache(repoRoot: string): SotCache;
10
+ export declare function writeSotCache(repoRoot: string, cache: SotCache): string;
11
+ export declare function setSotCacheEntry(cache: SotCache, decId: string, entry: SotCacheEntry): SotCache;
12
+ export declare function getSotCacheEntry(cache: SotCache, decId: string): SotCacheEntry | null;
13
+ export declare function deleteSotCacheEntry(cache: SotCache, decId: string): SotCache;
14
+ /**
15
+ * Iterate all entries. Layer A's pre-filter pass calls this on each
16
+ * Write to compute Jaccard against every cached DEC body.
17
+ */
18
+ export declare function sotCacheEntries(cache: SotCache): SotCacheEntry[];