@isaacriehm/cairn-core 0.4.1 → 0.4.3

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 (71) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/attention/bulk-accept.js +1 -1
  3. package/dist/attention/bulk-accept.js.map +1 -1
  4. package/dist/attention/dedup.d.ts +2 -2
  5. package/dist/attention/dedup.js +15 -4
  6. package/dist/attention/dedup.js.map +1 -1
  7. package/dist/attention/index.d.ts +1 -0
  8. package/dist/attention/index.js +1 -0
  9. package/dist/attention/index.js.map +1 -1
  10. package/dist/attention/restore.js +1 -1
  11. package/dist/attention/restore.js.map +1 -1
  12. package/dist/attention/serve/api.d.ts +23 -0
  13. package/dist/attention/serve/api.js +344 -0
  14. package/dist/attention/serve/api.js.map +1 -0
  15. package/dist/attention/serve/index.d.ts +62 -0
  16. package/dist/attention/serve/index.js +205 -0
  17. package/dist/attention/serve/index.js.map +1 -0
  18. package/dist/decision-capture/id.d.ts +62 -25
  19. package/dist/decision-capture/id.js +78 -57
  20. package/dist/decision-capture/id.js.map +1 -1
  21. package/dist/decision-capture/index.d.ts +3 -3
  22. package/dist/decision-capture/index.js +3 -3
  23. package/dist/decision-capture/index.js.map +1 -1
  24. package/dist/ground/schemas.js +2 -2
  25. package/dist/ground/schemas.js.map +1 -1
  26. package/dist/ground/scope-index.js +2 -2
  27. package/dist/ground/scope-index.js.map +1 -1
  28. package/dist/hooks/post-tool-use/citation-scanner.d.ts +1 -1
  29. package/dist/hooks/post-tool-use/citation-scanner.js +3 -3
  30. package/dist/hooks/post-tool-use/citation-scanner.js.map +1 -1
  31. package/dist/hooks/post-tool-use/copy-scanner.js +1 -1
  32. package/dist/hooks/post-tool-use/copy-scanner.js.map +1 -1
  33. package/dist/hooks/post-tool-use/legend-builder.d.ts +1 -1
  34. package/dist/hooks/post-tool-use/legend-builder.js +2 -2
  35. package/dist/hooks/post-tool-use/legend-builder.js.map +1 -1
  36. package/dist/init/ingest-docs.js +10 -6
  37. package/dist/init/ingest-docs.js.map +1 -1
  38. package/dist/init/mapper-parallel.js +1 -1
  39. package/dist/init/mapper-parallel.js.map +1 -1
  40. package/dist/init/rules-merge/ingest.js +9 -2
  41. package/dist/init/rules-merge/ingest.js.map +1 -1
  42. package/dist/init/source-comments/ingest.js +16 -4
  43. package/dist/init/source-comments/ingest.js.map +1 -1
  44. package/dist/mcp/bootstrap-guard.d.ts +19 -8
  45. package/dist/mcp/bootstrap-guard.js +41 -11
  46. package/dist/mcp/bootstrap-guard.js.map +1 -1
  47. package/dist/mcp/history/summarizer.js +1 -1
  48. package/dist/mcp/history/summarizer.js.map +1 -1
  49. package/dist/mcp/schemas.d.ts +1 -1
  50. package/dist/mcp/schemas.js +5 -5
  51. package/dist/mcp/schemas.js.map +1 -1
  52. package/dist/mcp/tools/archive.js +1 -1
  53. package/dist/mcp/tools/archive.js.map +1 -1
  54. package/dist/mcp/tools/attention-restore.js +1 -1
  55. package/dist/mcp/tools/attention-restore.js.map +1 -1
  56. package/dist/mcp/tools/attention-serve.d.ts +23 -0
  57. package/dist/mcp/tools/attention-serve.js +78 -0
  58. package/dist/mcp/tools/attention-serve.js.map +1 -0
  59. package/dist/mcp/tools/attention-wait.d.ts +18 -0
  60. package/dist/mcp/tools/attention-wait.js +74 -0
  61. package/dist/mcp/tools/attention-wait.js.map +1 -0
  62. package/dist/mcp/tools/index.js +4 -0
  63. package/dist/mcp/tools/index.js.map +1 -1
  64. package/dist/mcp/tools/record-decision.js +14 -2
  65. package/dist/mcp/tools/record-decision.js.map +1 -1
  66. package/dist/mcp/tools/resolve-attention.js +2 -2
  67. package/dist/mcp/tools/resolve-attention.js.map +1 -1
  68. package/package.json +1 -1
  69. package/templates/attention-ui/app.css +406 -0
  70. package/templates/attention-ui/app.js +384 -0
  71. package/templates/attention-ui/index.html +56 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Browser-based DEC-draft triage GUI.
3
+ *
4
+ * Spawned by `cairn attention serve` (CLI) or
5
+ * `cairn_attention_serve` (MCP). Operator drains the inbox in the
6
+ * browser instead of through `AskUserQuestion` round-trips, then
7
+ * clicks "I'm done" — the server writes a sentinel at
8
+ * `.cairn/cache/attention-done.json` that the caller polls (via
9
+ * `cairn_attention_wait` or by tailing the file).
10
+ *
11
+ * Why a GUI: at >15 drafts the inline `AskUserQuestion` flow burns
12
+ * `cairn_decision_get` calls per draft and 4-cap-per-question batches
13
+ * the operator through dozens of MCP turns. The browser does all the
14
+ * I/O directly against `.cairn/`, dropping per-triage round-trips to
15
+ * zero.
16
+ *
17
+ * Mechanics:
18
+ * - HTTP server on a free port (or operator-supplied), bound to
19
+ * 127.0.0.1 so the surface is local-only.
20
+ * - JSON API mirrors the existing attention handlers (bulk-accept,
21
+ * dedup, resolve, restore) so all writes funnel through the same
22
+ * `withWriteLock` path the MCP tools use.
23
+ * - Static SPA bundle (vanilla HTML+JS+CSS) under
24
+ * `cairn-core/templates/attention-ui/` — no build step.
25
+ * - Idle timeout: 10 min default, reset by `/api/heartbeat`. Server
26
+ * shuts down when idle exceeds the timeout or on `/api/done`.
27
+ */
28
+ /** Read-only accessor for `cairn_attention_wait` and tests. */
29
+ export declare function getActiveAttentionServer(repoRoot: string): AttentionServeHandle | undefined;
30
+ export interface AttentionServeOptions {
31
+ repoRoot: string;
32
+ /** Listen port. Pass 0 to let the OS pick. */
33
+ port: number;
34
+ /** Idle (no heartbeat / no API activity) before auto-shutdown. */
35
+ idleTimeoutMs?: number;
36
+ /** Optional caller-supplied abort signal. */
37
+ signal?: AbortSignal;
38
+ }
39
+ export interface AttentionServeHandle {
40
+ port: number;
41
+ url: string;
42
+ sentinelPath: string;
43
+ /** Resolves once the server has shut down (operator clicked Done or idled out). */
44
+ done: Promise<DoneState>;
45
+ /** Force shutdown — typically wired to SIGINT / SIGTERM in CLI. */
46
+ close: () => Promise<void>;
47
+ }
48
+ export interface DoneState {
49
+ reason: "done" | "idle" | "abort";
50
+ accepted: number;
51
+ rejected: number;
52
+ merged: number;
53
+ edited: number;
54
+ startedAt: string;
55
+ endedAt: string;
56
+ }
57
+ /**
58
+ * Boot the triage server. Returns once the listener is ready;
59
+ * `handle.done` resolves when the operator finishes or the server
60
+ * idles out.
61
+ */
62
+ export declare function startAttentionServer(opts: AttentionServeOptions): Promise<AttentionServeHandle>;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Browser-based DEC-draft triage GUI.
3
+ *
4
+ * Spawned by `cairn attention serve` (CLI) or
5
+ * `cairn_attention_serve` (MCP). Operator drains the inbox in the
6
+ * browser instead of through `AskUserQuestion` round-trips, then
7
+ * clicks "I'm done" — the server writes a sentinel at
8
+ * `.cairn/cache/attention-done.json` that the caller polls (via
9
+ * `cairn_attention_wait` or by tailing the file).
10
+ *
11
+ * Why a GUI: at >15 drafts the inline `AskUserQuestion` flow burns
12
+ * `cairn_decision_get` calls per draft and 4-cap-per-question batches
13
+ * the operator through dozens of MCP turns. The browser does all the
14
+ * I/O directly against `.cairn/`, dropping per-triage round-trips to
15
+ * zero.
16
+ *
17
+ * Mechanics:
18
+ * - HTTP server on a free port (or operator-supplied), bound to
19
+ * 127.0.0.1 so the surface is local-only.
20
+ * - JSON API mirrors the existing attention handlers (bulk-accept,
21
+ * dedup, resolve, restore) so all writes funnel through the same
22
+ * `withWriteLock` path the MCP tools use.
23
+ * - Static SPA bundle (vanilla HTML+JS+CSS) under
24
+ * `cairn-core/templates/attention-ui/` — no build step.
25
+ * - Idle timeout: 10 min default, reset by `/api/heartbeat`. Server
26
+ * shuts down when idle exceeds the timeout or on `/api/done`.
27
+ */
28
+ import { createServer } from "node:http";
29
+ import { dirname, join } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
32
+ import { logger } from "../../logger.js";
33
+ import { handleApi } from "./api.js";
34
+ const log = logger("attention.serve");
35
+ const HERE = dirname(fileURLToPath(import.meta.url));
36
+ /**
37
+ * dist/attention/serve/index.js → walk up to package root, then into
38
+ * templates/attention-ui/. Bundled layout co-locates templates as a
39
+ * sibling of dist/cli.mjs (mirrors the seed.ts pattern).
40
+ */
41
+ const TEMPLATES_ROOT = typeof __CAIRN_BUNDLED__ !== "undefined" && __CAIRN_BUNDLED__
42
+ ? join(HERE, "templates", "attention-ui")
43
+ : join(HERE, "..", "..", "..", "templates", "attention-ui");
44
+ const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
45
+ const DONE_TIMEOUT_GRACE_MS = 500;
46
+ /**
47
+ * Per-repoRoot live-server registry. The MCP server is a single
48
+ * long-lived process; both `cairn_attention_serve` and
49
+ * `cairn_attention_wait` share this map so wait can await the live
50
+ * `done` promise instead of polling.
51
+ */
52
+ const liveServers = new Map();
53
+ /** Read-only accessor for `cairn_attention_wait` and tests. */
54
+ export function getActiveAttentionServer(repoRoot) {
55
+ return liveServers.get(repoRoot);
56
+ }
57
+ /**
58
+ * Boot the triage server. Returns once the listener is ready;
59
+ * `handle.done` resolves when the operator finishes or the server
60
+ * idles out.
61
+ */
62
+ export async function startAttentionServer(opts) {
63
+ const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
64
+ const sentinelPath = join(opts.repoRoot, ".cairn", "cache", "attention-done.json");
65
+ // Pre-clear any stale sentinel so the caller's wait loop isn't
66
+ // tricked by a previous run's payload.
67
+ try {
68
+ rmSync(sentinelPath, { force: true });
69
+ }
70
+ catch {
71
+ /* best-effort */
72
+ }
73
+ const counters = { accepted: 0, rejected: 0, merged: 0, edited: 0 };
74
+ const startedAt = new Date().toISOString();
75
+ let lastActivity = Date.now();
76
+ const touch = () => {
77
+ lastActivity = Date.now();
78
+ };
79
+ let resolveDone;
80
+ const donePromise = new Promise((resolve) => {
81
+ resolveDone = resolve;
82
+ });
83
+ let server;
84
+ let idleTimer;
85
+ let shutdownStarted = false;
86
+ const writeSentinel = (reason) => {
87
+ const state = {
88
+ reason,
89
+ ...counters,
90
+ startedAt,
91
+ endedAt: new Date().toISOString(),
92
+ };
93
+ try {
94
+ mkdirSync(dirname(sentinelPath), { recursive: true });
95
+ writeFileSync(sentinelPath, JSON.stringify(state, null, 2), "utf8");
96
+ }
97
+ catch (err) {
98
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "failed to write sentinel");
99
+ }
100
+ return state;
101
+ };
102
+ const beginShutdown = (reason) => {
103
+ if (shutdownStarted)
104
+ return;
105
+ shutdownStarted = true;
106
+ clearInterval(idleTimer);
107
+ const state = writeSentinel(reason);
108
+ setTimeout(() => {
109
+ server.close(() => {
110
+ resolveDone(state);
111
+ });
112
+ }, DONE_TIMEOUT_GRACE_MS);
113
+ };
114
+ server = createServer((req, res) => {
115
+ void handleRequest(req, res, {
116
+ repoRoot: opts.repoRoot,
117
+ counters,
118
+ touch,
119
+ onDone: () => beginShutdown("done"),
120
+ });
121
+ });
122
+ await new Promise((resolve, reject) => {
123
+ server.once("error", reject);
124
+ server.listen(opts.port, "127.0.0.1", () => {
125
+ server.off("error", reject);
126
+ resolve();
127
+ });
128
+ });
129
+ const addr = server.address();
130
+ const port = typeof addr === "object" && addr !== null ? addr.port : opts.port;
131
+ const url = `http://127.0.0.1:${port}/`;
132
+ idleTimer = setInterval(() => {
133
+ if (Date.now() - lastActivity >= idleTimeoutMs) {
134
+ log.info({ idleTimeoutMs }, "attention server idle — shutting down");
135
+ beginShutdown("idle");
136
+ }
137
+ }, 30_000);
138
+ if (opts.signal !== undefined) {
139
+ if (opts.signal.aborted) {
140
+ beginShutdown("abort");
141
+ }
142
+ else {
143
+ opts.signal.addEventListener("abort", () => beginShutdown("abort"), {
144
+ once: true,
145
+ });
146
+ }
147
+ }
148
+ log.info({ port, url, sentinelPath }, "attention server listening");
149
+ const handle = {
150
+ port,
151
+ url,
152
+ sentinelPath,
153
+ done: donePromise,
154
+ close: async () => {
155
+ beginShutdown("abort");
156
+ await donePromise;
157
+ },
158
+ };
159
+ liveServers.set(opts.repoRoot, handle);
160
+ void donePromise.finally(() => liveServers.delete(opts.repoRoot));
161
+ return handle;
162
+ }
163
+ async function handleRequest(req, res, ctx) {
164
+ ctx.touch();
165
+ const url = req.url ?? "/";
166
+ if (url === "/" || url === "/index.html") {
167
+ return serveStatic(res, "index.html", "text/html; charset=utf-8");
168
+ }
169
+ if (url === "/static/app.js") {
170
+ return serveStatic(res, "app.js", "application/javascript; charset=utf-8");
171
+ }
172
+ if (url === "/static/app.css") {
173
+ return serveStatic(res, "app.css", "text/css; charset=utf-8");
174
+ }
175
+ if (url.startsWith("/api/")) {
176
+ return handleApi(req, res, ctx);
177
+ }
178
+ res.statusCode = 404;
179
+ res.setHeader("content-type", "text/plain");
180
+ res.end("not found");
181
+ }
182
+ function serveStatic(res, filename, contentType) {
183
+ const path = join(TEMPLATES_ROOT, filename);
184
+ if (!existsSync(path)) {
185
+ res.statusCode = 500;
186
+ res.setHeader("content-type", "text/plain");
187
+ res.end(`attention-ui template missing: ${filename}`);
188
+ return;
189
+ }
190
+ let body;
191
+ try {
192
+ body = readFileSync(path, "utf8");
193
+ }
194
+ catch (err) {
195
+ res.statusCode = 500;
196
+ res.setHeader("content-type", "text/plain");
197
+ res.end(`attention-ui template read failed: ${err instanceof Error ? err.message : String(err)}`);
198
+ return;
199
+ }
200
+ res.statusCode = 200;
201
+ res.setHeader("content-type", contentType);
202
+ res.setHeader("cache-control", "no-store");
203
+ res.end(body);
204
+ }
205
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/attention/serve/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAC;AACjG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACL,UAAU,EACV,SAAS,EACT,YAAY,EACZ,MAAM,EACN,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,MAAM,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD;;;;GAIG;AACH,MAAM,cAAc,GAClB,OAAO,iBAAiB,KAAK,WAAW,IAAI,iBAAiB;IAC3D,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,cAAc,CAAC;IACzC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AAEhE,MAAM,uBAAuB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC/C,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAElC;;;;;GAKG;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,EAAgC,CAAC;AAE5D,+DAA+D;AAC/D,MAAM,UAAU,wBAAwB,CACtC,QAAgB;IAEhB,OAAO,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC;AAgCD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAA2B;IAE3B,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,uBAAuB,CAAC;IACpE,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACnF,+DAA+D;IAC/D,uCAAuC;IACvC,IAAI,CAAC;QACH,MAAM,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,iBAAiB;IACnB,CAAC;IAED,MAAM,QAAQ,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACpE,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE3C,IAAI,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,GAAS,EAAE;QACvB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC5B,CAAC,CAAC;IAEF,IAAI,WAAwC,CAAC;IAC7C,MAAM,WAAW,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QACrD,WAAW,GAAG,OAAO,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,IAAI,MAAc,CAAC;IACnB,IAAI,SAAyB,CAAC;IAC9B,IAAI,eAAe,GAAG,KAAK,CAAC;IAE5B,MAAM,aAAa,GAAG,CAAC,MAA2B,EAAa,EAAE;QAC/D,MAAM,KAAK,GAAc;YACvB,MAAM;YACN,GAAG,QAAQ;YACX,SAAS;YACT,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SAClC,CAAC;QACF,IAAI,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CACN,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EACzD,0BAA0B,CAC3B,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,CAAC,MAA2B,EAAQ,EAAE;QAC1D,IAAI,eAAe;YAAE,OAAO;QAC5B,eAAe,GAAG,IAAI,CAAC;QACvB,aAAa,CAAC,SAAS,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACpC,UAAU,CAAC,GAAG,EAAE;YACd,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;gBAChB,WAAW,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CAAC,CAAC;QACL,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACjC,KAAK,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,QAAQ;YACR,KAAK;YACL,MAAM,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC;SACpC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YACzC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IAC9B,MAAM,IAAI,GACR,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IACpE,MAAM,GAAG,GAAG,oBAAoB,IAAI,GAAG,CAAC;IAExC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,IAAI,aAAa,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,EAAE,uCAAuC,CAAC,CAAC;YACrE,aAAa,CAAC,MAAM,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,EAAE,MAAM,CAAC,CAAC;IAEX,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACxB,aAAa,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE;gBAClE,IAAI,EAAE,IAAI;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,4BAA4B,CAAC,CAAC;IAEpE,MAAM,MAAM,GAAyB;QACnC,IAAI;QACJ,GAAG;QACH,YAAY;QACZ,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,aAAa,CAAC,OAAO,CAAC,CAAC;YACvB,MAAM,WAAW,CAAC;QACpB,CAAC;KACF,CAAC;IACF,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACvC,KAAK,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC;AAChB,CAAC;AASD,KAAK,UAAU,aAAa,CAC1B,GAAoB,EACpB,GAAmB,EACnB,GAAqB;IAErB,GAAG,CAAC,KAAK,EAAE,CAAC;IACZ,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;IAE3B,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,aAAa,EAAE,CAAC;QACzC,OAAO,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,0BAA0B,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,GAAG,KAAK,gBAAgB,EAAE,CAAC;QAC7B,OAAO,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,uCAAuC,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,GAAG,KAAK,iBAAiB,EAAE,CAAC;QAC9B,OAAO,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,yBAAyB,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;IACD,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;IACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAC5C,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,WAAW,CAClB,GAAmB,EACnB,QAAgB,EAChB,WAAmB;IAEnB,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;QACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAC5C,GAAG,CAAC,GAAG,CAAC,kCAAkC,QAAQ,EAAE,CAAC,CAAC;QACtD,OAAO;IACT,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;QACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAC5C,GAAG,CAAC,GAAG,CACL,sCAAsC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACzF,CAAC;QACF,OAAO;IACT,CAAC;IACD,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;IACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAC3C,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAC3C,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
@@ -1,38 +1,75 @@
1
1
  /**
2
- * Decision + invariant id allocators. Monotonic, never reused.
2
+ * Content-addressed id derivation for decisions and invariants.
3
3
  *
4
- * Decisions: scan `.cairn/ground/decisions/` for accepted decisions AND
5
- * `.cairn/ground/decisions/_inbox/` for outstanding drafts. Either counts
6
- * toward the high-water mark a draft that's pending operator confirmation
7
- * still owns its id; rejecting a draft does NOT recycle the id.
4
+ * Decisions: `DEC-<hash>` where `<hash>` is the first 7 hex chars of
5
+ * sha256(canonicalized input). Stable across clones two devs
6
+ * that capture the same source comment in the same file produce
7
+ * the same id, so concurrent adoption runs do not collide on merge.
8
8
  *
9
- * Invariants: scan `.cairn/ground/invariants/INV-<NNNN>.md`. Phase 7b writes
10
- * invariants directly to ground state (no `_inbox/` flow — they auto-promote
11
- * from the constraint classifier; operator edits / supersedes after the
12
- * fact).
9
+ * Invariants: same shape, `INV-<hash>`.
13
10
  *
14
- * Single source of truth for id allocation. The MCP write tools and the
15
- * init pipeline call these helpers; do NOT re-implement the scan elsewhere.
11
+ * On the rare hash collision against an existing on-disk id with
12
+ * different content, the new id extends to 8 chars. Same fallback git
13
+ * uses for short SHAs.
14
+ *
15
+ * Ids are never recycled — rejecting a draft renames the file to
16
+ * `<id>.rejected.md` so the same hash never re-allocates to a
17
+ * different decision.
16
18
  */
19
+ export interface DecisionIdInput {
20
+ /** Title — required. Lowercased + trimmed in the hash input. */
21
+ title: string;
22
+ /** Free-text rationale or summary. */
23
+ rationale?: string;
24
+ /** Provenance source (e.g. `init-source-comments`, `init-rules-merge`, `user-record`). */
25
+ capture_source?: string;
26
+ /** Source file the decision was extracted from. */
27
+ source_file?: string;
28
+ /** Line / offset within the source file. */
29
+ source_offset?: number;
30
+ /** Original raw comment / section text. */
31
+ raw?: string;
32
+ /** Scope globs (sorted before hashing for stability). */
33
+ scope_globs?: string[];
34
+ /** Full body markdown (for manual `cairn_record_decision` calls). */
35
+ body_markdown?: string;
36
+ /**
37
+ * Millisecond timestamp — only set for manual user-record paths
38
+ * where there is no stable provenance. Source-comment / rules-merge
39
+ * derived ids omit this so re-running the pipeline is idempotent.
40
+ */
41
+ timestamp_ms?: number;
42
+ }
43
+ export interface InvariantIdInput {
44
+ /** Title — required. Lowercased + trimmed. */
45
+ title: string;
46
+ /** Source file the constraint was extracted from. */
47
+ source_file?: string;
48
+ /** Line / offset within the source file. */
49
+ source_offset?: number;
50
+ /** Original raw comment text. */
51
+ raw?: string;
52
+ /** Millisecond timestamp — manual writes only. */
53
+ timestamp_ms?: number;
54
+ }
17
55
  /**
18
- * Scan both the canonical decisions dir and the `_inbox/` for
19
- * DEC-NNNN-prefixed files; return the set of ids found.
56
+ * Compute a stable `DEC-<hash>` id from the canonical input. When
57
+ * `existing` is supplied and the 7-char prefix collides with an id
58
+ * already in that set whose content differs, the id extends to 8+
59
+ * chars until unique.
20
60
  */
21
- export declare function scanExistingDecisionIds(repoRoot: string): Set<string>;
61
+ export declare function computeDecisionId(input: DecisionIdInput, existing?: Set<string>): string;
22
62
  /**
23
- * Return the next free `DEC-<NNNN>` id, optionally factoring in a
24
- * caller-supplied set (e.g. ids the MCP tool just validated against).
63
+ * Compute a stable `INV-<hash>` id from the canonical input. Same
64
+ * collision-extension behavior as `computeDecisionId`.
25
65
  */
26
- export declare function allocateDecisionId(repoRoot: string, existing?: Set<string>): string;
66
+ export declare function computeInvariantId(input: InvariantIdInput, existing?: Set<string>): string;
27
67
  /**
28
- * Scan `.cairn/ground/invariants/` for INV-<NNNN>-prefixed files; return the
29
- * set of ids found.
68
+ * Scan both the canonical decisions dir and `_inbox/` for
69
+ * `DEC-<hash>` filenames; return the set of ids found.
30
70
  */
31
- export declare function scanExistingInvariantIds(repoRoot: string): Set<string>;
71
+ export declare function scanExistingDecisionIds(repoRoot: string): Set<string>;
32
72
  /**
33
- * Return the next free `INV-<NNNN>` id — matches the schema regex
34
- * at packages/cairn-core/src/ground/schemas.ts. Optionally factor in
35
- * a caller-supplied set so a batch of allocations doesn't collide on
36
- * disk before any are written.
73
+ * Scan `.cairn/ground/invariants/` for `INV-<hash>` filenames.
37
74
  */
38
- export declare function allocateInvariantId(repoRoot: string, existing?: Set<string>): string;
75
+ export declare function scanExistingInvariantIds(repoRoot: string): Set<string>;
@@ -1,29 +1,87 @@
1
1
  /**
2
- * Decision + invariant id allocators. Monotonic, never reused.
2
+ * Content-addressed id derivation for decisions and invariants.
3
3
  *
4
- * Decisions: scan `.cairn/ground/decisions/` for accepted decisions AND
5
- * `.cairn/ground/decisions/_inbox/` for outstanding drafts. Either counts
6
- * toward the high-water mark a draft that's pending operator confirmation
7
- * still owns its id; rejecting a draft does NOT recycle the id.
4
+ * Decisions: `DEC-<hash>` where `<hash>` is the first 7 hex chars of
5
+ * sha256(canonicalized input). Stable across clones two devs
6
+ * that capture the same source comment in the same file produce
7
+ * the same id, so concurrent adoption runs do not collide on merge.
8
8
  *
9
- * Invariants: scan `.cairn/ground/invariants/INV-<NNNN>.md`. Phase 7b writes
10
- * invariants directly to ground state (no `_inbox/` flow — they auto-promote
11
- * from the constraint classifier; operator edits / supersedes after the
12
- * fact).
9
+ * Invariants: same shape, `INV-<hash>`.
13
10
  *
14
- * Single source of truth for id allocation. The MCP write tools and the
15
- * init pipeline call these helpers; do NOT re-implement the scan elsewhere.
11
+ * On the rare hash collision against an existing on-disk id with
12
+ * different content, the new id extends to 8 chars. Same fallback git
13
+ * uses for short SHAs.
14
+ *
15
+ * Ids are never recycled — rejecting a draft renames the file to
16
+ * `<id>.rejected.md` so the same hash never re-allocates to a
17
+ * different decision.
16
18
  */
19
+ import { createHash } from "node:crypto";
17
20
  import { existsSync, readdirSync } from "node:fs";
18
21
  import { join } from "node:path";
19
22
  import { decisionsDir, invariantsDir } from "../ground/paths.js";
20
- const FILENAME_RE = /^DEC-(\d{4,})(?:\.draft|\.rejected)?\.md$/;
21
- // Invariant filename: `INV-<NNNN>.md` — matches the schema id
22
- // regex /^INV-\d{4,}$/ at packages/cairn-core/src/ground/schemas.ts.
23
- const INVARIANT_FILENAME_RE = /^INV-(\d{4,})\.md$/;
23
+ /** Default short-hash length (matches git short-SHA convention). */
24
+ const HASH_LEN = 7;
25
+ /** DEC filename: `DEC-<hex>.md`, optionally `.draft` or `.rejected`. */
26
+ const FILENAME_RE = /^DEC-([0-9a-f]{7,})(?:\.draft|\.rejected)?\.md$/;
27
+ /** INV filename: `INV-<hex>.md`. Matches the schema id regex in `ground/schemas.ts`. */
28
+ const INVARIANT_FILENAME_RE = /^INV-([0-9a-f]{7,})\.md$/;
29
+ function canonicalDecision(input) {
30
+ return JSON.stringify({
31
+ title: input.title.trim().toLowerCase(),
32
+ rationale: input.rationale ?? null,
33
+ capture_source: input.capture_source ?? null,
34
+ source_file: input.source_file ?? null,
35
+ source_offset: input.source_offset ?? null,
36
+ raw: input.raw ?? null,
37
+ scope_globs: input.scope_globs !== undefined ? [...input.scope_globs].sort() : null,
38
+ body_markdown: input.body_markdown ?? null,
39
+ timestamp_ms: input.timestamp_ms ?? null,
40
+ });
41
+ }
42
+ function canonicalInvariant(input) {
43
+ return JSON.stringify({
44
+ title: input.title.trim().toLowerCase(),
45
+ source_file: input.source_file ?? null,
46
+ source_offset: input.source_offset ?? null,
47
+ raw: input.raw ?? null,
48
+ timestamp_ms: input.timestamp_ms ?? null,
49
+ });
50
+ }
51
+ /**
52
+ * Compute a stable `DEC-<hash>` id from the canonical input. When
53
+ * `existing` is supplied and the 7-char prefix collides with an id
54
+ * already in that set whose content differs, the id extends to 8+
55
+ * chars until unique.
56
+ */
57
+ export function computeDecisionId(input, existing) {
58
+ const digest = createHash("sha256").update(canonicalDecision(input)).digest("hex");
59
+ for (let len = HASH_LEN; len <= digest.length; len++) {
60
+ const candidate = `DEC-${digest.slice(0, len)}`;
61
+ if (existing === undefined || !existing.has(candidate))
62
+ return candidate;
63
+ }
64
+ throw new Error("computeDecisionId: hash exhaustion (impossible at sha256)");
65
+ }
66
+ /**
67
+ * Compute a stable `INV-<hash>` id from the canonical input. Same
68
+ * collision-extension behavior as `computeDecisionId`.
69
+ */
70
+ export function computeInvariantId(input, existing) {
71
+ const digest = createHash("sha256").update(canonicalInvariant(input)).digest("hex");
72
+ for (let len = HASH_LEN; len <= digest.length; len++) {
73
+ const candidate = `INV-${digest.slice(0, len)}`;
74
+ if (existing === undefined || !existing.has(candidate))
75
+ return candidate;
76
+ }
77
+ throw new Error("computeInvariantId: hash exhaustion (impossible at sha256)");
78
+ }
79
+ /* -------------------------------------------------------------------------- */
80
+ /* On-disk scans (used for collision check + auxiliary lookups) */
81
+ /* -------------------------------------------------------------------------- */
24
82
  /**
25
- * Scan both the canonical decisions dir and the `_inbox/` for
26
- * DEC-NNNN-prefixed files; return the set of ids found.
83
+ * Scan both the canonical decisions dir and `_inbox/` for
84
+ * `DEC-<hash>` filenames; return the set of ids found.
27
85
  */
28
86
  export function scanExistingDecisionIds(repoRoot) {
29
87
  const dir = decisionsDir(repoRoot);
@@ -43,31 +101,13 @@ export function scanExistingDecisionIds(repoRoot) {
43
101
  const match = name.match(FILENAME_RE);
44
102
  if (!match || !match[1])
45
103
  continue;
46
- ids.add(`DEC-${match[1].padStart(4, "0")}`);
104
+ ids.add(`DEC-${match[1]}`);
47
105
  }
48
106
  }
49
107
  return ids;
50
108
  }
51
109
  /**
52
- * Return the next free `DEC-<NNNN>` id, optionally factoring in a
53
- * caller-supplied set (e.g. ids the MCP tool just validated against).
54
- */
55
- export function allocateDecisionId(repoRoot, existing) {
56
- const ids = existing ?? scanExistingDecisionIds(repoRoot);
57
- let max = 0;
58
- for (const id of ids) {
59
- const m = id.match(/^DEC-(\d+)$/);
60
- if (!m?.[1])
61
- continue;
62
- const n = Number.parseInt(m[1], 10);
63
- if (Number.isFinite(n) && n > max)
64
- max = n;
65
- }
66
- return `DEC-${(max + 1).toString().padStart(4, "0")}`;
67
- }
68
- /**
69
- * Scan `.cairn/ground/invariants/` for INV-<NNNN>-prefixed files; return the
70
- * set of ids found.
110
+ * Scan `.cairn/ground/invariants/` for `INV-<hash>` filenames.
71
111
  */
72
112
  export function scanExistingInvariantIds(repoRoot) {
73
113
  const dir = invariantsDir(repoRoot);
@@ -85,27 +125,8 @@ export function scanExistingInvariantIds(repoRoot) {
85
125
  const match = name.match(INVARIANT_FILENAME_RE);
86
126
  if (!match || !match[1])
87
127
  continue;
88
- ids.add(`INV-${match[1].padStart(4, "0")}`);
128
+ ids.add(`INV-${match[1]}`);
89
129
  }
90
130
  return ids;
91
131
  }
92
- /**
93
- * Return the next free `INV-<NNNN>` id — matches the schema regex
94
- * at packages/cairn-core/src/ground/schemas.ts. Optionally factor in
95
- * a caller-supplied set so a batch of allocations doesn't collide on
96
- * disk before any are written.
97
- */
98
- export function allocateInvariantId(repoRoot, existing) {
99
- const ids = existing ?? scanExistingInvariantIds(repoRoot);
100
- let max = 0;
101
- for (const id of ids) {
102
- const m = id.match(/^INV-(\d+)$/);
103
- if (!m?.[1])
104
- continue;
105
- const n = Number.parseInt(m[1], 10);
106
- if (Number.isFinite(n) && n > max)
107
- max = n;
108
- }
109
- return `INV-${(max + 1).toString().padStart(4, "0")}`;
110
- }
111
132
  //# sourceMappingURL=id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/decision-capture/id.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/decision-capture/id.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Decision-capture surface.
3
3
  *
4
- * Only the monotonic id allocators remain — the Tier-1 LLM extractor +
5
- * refinement pipeline was orchestrator-era code (auto-extract DECs from
4
+ * Only the content-addressed id helpers remain — the Tier-1 LLM extractor
5
+ * + refinement pipeline was orchestrator-era code (auto-extract DECs from
6
6
  * sessions) that is no longer wired into the plugin flow. Operator-driven
7
7
  * DEC creation lives in the `cairn-direction` skill + the
8
8
  * `cairn_record_decision` MCP tool now.
9
9
  */
10
- export { allocateDecisionId, allocateInvariantId, scanExistingDecisionIds, scanExistingInvariantIds, } from "./id.js";
10
+ export { computeDecisionId, computeInvariantId, scanExistingDecisionIds, scanExistingInvariantIds, type DecisionIdInput, type InvariantIdInput, } from "./id.js";
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Decision-capture surface.
3
3
  *
4
- * Only the monotonic id allocators remain — the Tier-1 LLM extractor +
5
- * refinement pipeline was orchestrator-era code (auto-extract DECs from
4
+ * Only the content-addressed id helpers remain — the Tier-1 LLM extractor
5
+ * + refinement pipeline was orchestrator-era code (auto-extract DECs from
6
6
  * sessions) that is no longer wired into the plugin flow. Operator-driven
7
7
  * DEC creation lives in the `cairn-direction` skill + the
8
8
  * `cairn_record_decision` MCP tool now.
9
9
  */
10
- export { allocateDecisionId, allocateInvariantId, scanExistingDecisionIds, scanExistingInvariantIds, } from "./id.js";
10
+ export { computeDecisionId, computeInvariantId, scanExistingDecisionIds, scanExistingInvariantIds, } from "./id.js";
11
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/decision-capture/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/decision-capture/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,GAGzB,MAAM,SAAS,CAAC"}
@@ -108,7 +108,7 @@ export const DecisionAssertion = z.discriminatedUnion("kind", [
108
108
  ]);
109
109
  export const DecisionFrontmatter = z
110
110
  .object({
111
- id: z.string().regex(/^DEC-\d{4,}$/, "decision id must match DEC-NNNN"),
111
+ id: z.string().regex(/^DEC-[0-9a-f]{7,}$/, "decision id must match DEC-<hash7>"),
112
112
  title: z.string(),
113
113
  type: z.literal("adr").optional(),
114
114
  status: z
@@ -133,7 +133,7 @@ export const DecisionFrontmatter = z
133
133
  .passthrough();
134
134
  export const InvariantFrontmatter = z
135
135
  .object({
136
- id: z.string().regex(/^INV-\d{4,}$/, "invariant id must match INV-NNNN"),
136
+ id: z.string().regex(/^INV-[0-9a-f]{7,}$/, "invariant id must match INV-<hash7>"),
137
137
  title: z.string(),
138
138
  type: z.literal("invariant").optional(),
139
139
  status: z.enum(["active", "superseded"]).optional(),