@isaacriehm/cairn-core 0.4.2 → 0.5.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/align-undo/index.d.ts +6 -0
- package/dist/align-undo/index.js +6 -0
- package/dist/align-undo/index.js.map +1 -0
- package/dist/align-undo/log.d.ts +53 -0
- package/dist/align-undo/log.js +99 -0
- package/dist/align-undo/log.js.map +1 -0
- package/dist/align-undo/undo.d.ts +66 -0
- package/dist/align-undo/undo.js +187 -0
- package/dist/align-undo/undo.js.map +1 -0
- package/dist/attention/bulk-accept.js +1 -1
- package/dist/attention/bulk-accept.js.map +1 -1
- package/dist/attention/dedup.d.ts +2 -2
- package/dist/attention/dedup.js +16 -51
- package/dist/attention/dedup.js.map +1 -1
- package/dist/attention/index.d.ts +1 -0
- package/dist/attention/index.js +1 -0
- package/dist/attention/index.js.map +1 -1
- package/dist/attention/restore.js +1 -1
- package/dist/attention/restore.js.map +1 -1
- package/dist/attention/serve/api.d.ts +23 -0
- package/dist/attention/serve/api.js +344 -0
- package/dist/attention/serve/api.js.map +1 -0
- package/dist/attention/serve/index.d.ts +62 -0
- package/dist/attention/serve/index.js +205 -0
- package/dist/attention/serve/index.js.map +1 -0
- package/dist/decision-capture/id.d.ts +62 -25
- package/dist/decision-capture/id.js +78 -57
- package/dist/decision-capture/id.js.map +1 -1
- package/dist/decision-capture/index.d.ts +3 -3
- package/dist/decision-capture/index.js +3 -3
- package/dist/decision-capture/index.js.map +1 -1
- package/dist/drain/drain.d.ts +77 -0
- package/dist/drain/drain.js +464 -0
- package/dist/drain/drain.js.map +1 -0
- package/dist/drain/index.d.ts +5 -0
- package/dist/drain/index.js +5 -0
- package/dist/drain/index.js.map +1 -0
- package/dist/fix-align/index.d.ts +5 -0
- package/dist/fix-align/index.js +5 -0
- package/dist/fix-align/index.js.map +1 -0
- package/dist/fix-align/run.d.ts +99 -0
- package/dist/fix-align/run.js +258 -0
- package/dist/fix-align/run.js.map +1 -0
- package/dist/ground/alignment-pending.d.ts +28 -0
- package/dist/ground/alignment-pending.js +83 -0
- package/dist/ground/alignment-pending.js.map +1 -0
- package/dist/ground/anchor-map.d.ts +14 -0
- package/dist/ground/anchor-map.js +57 -0
- package/dist/ground/anchor-map.js.map +1 -0
- package/dist/ground/index.d.ts +9 -2
- package/dist/ground/index.js +8 -2
- package/dist/ground/index.js.map +1 -1
- package/dist/ground/paths.d.ts +21 -0
- package/dist/ground/paths.js +43 -0
- package/dist/ground/paths.js.map +1 -1
- package/dist/ground/schemas.d.ts +201 -0
- package/dist/ground/schemas.js +128 -3
- package/dist/ground/schemas.js.map +1 -1
- package/dist/ground/scope-index.js +2 -2
- package/dist/ground/scope-index.js.map +1 -1
- package/dist/ground/slug.d.ts +60 -0
- package/dist/ground/slug.js +103 -0
- package/dist/ground/slug.js.map +1 -0
- package/dist/ground/sot-bindings.d.ts +14 -0
- package/dist/ground/sot-bindings.js +80 -0
- package/dist/ground/sot-bindings.js.map +1 -0
- package/dist/ground/sot-cache.d.ts +18 -0
- package/dist/ground/sot-cache.js +63 -0
- package/dist/ground/sot-cache.js.map +1 -0
- package/dist/ground/topic-index.d.ts +20 -0
- package/dist/ground/topic-index.js +60 -0
- package/dist/ground/topic-index.js.map +1 -0
- package/dist/hooks/post-tool-use/citation-scanner.d.ts +1 -1
- package/dist/hooks/post-tool-use/citation-scanner.js +3 -3
- package/dist/hooks/post-tool-use/citation-scanner.js.map +1 -1
- package/dist/hooks/post-tool-use/copy-scanner.js +1 -1
- package/dist/hooks/post-tool-use/copy-scanner.js.map +1 -1
- package/dist/hooks/post-tool-use/index.d.ts +2 -0
- package/dist/hooks/post-tool-use/index.js +1 -0
- package/dist/hooks/post-tool-use/index.js.map +1 -1
- package/dist/hooks/post-tool-use/legend-builder.d.ts +1 -1
- package/dist/hooks/post-tool-use/legend-builder.js +2 -2
- package/dist/hooks/post-tool-use/legend-builder.js.map +1 -1
- package/dist/hooks/post-tool-use/sot-align.d.ts +166 -0
- package/dist/hooks/post-tool-use/sot-align.js +1311 -0
- package/dist/hooks/post-tool-use/sot-align.js.map +1 -0
- package/dist/hooks/pre-commit/index.d.ts +8 -0
- package/dist/hooks/pre-commit/index.js +8 -0
- package/dist/hooks/pre-commit/index.js.map +1 -0
- package/dist/hooks/pre-commit/sot-align-precommit.d.ts +60 -0
- package/dist/hooks/pre-commit/sot-align-precommit.js +221 -0
- package/dist/hooks/pre-commit/sot-align-precommit.js.map +1 -0
- package/dist/hooks/runners/session-start.js +41 -0
- package/dist/hooks/runners/session-start.js.map +1 -1
- package/dist/hooks/sot-align-common.d.ts +39 -0
- package/dist/hooks/sot-align-common.js +152 -0
- package/dist/hooks/sot-align-common.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/init/index.d.ts +4 -2
- package/dist/init/index.js +2 -1
- package/dist/init/index.js.map +1 -1
- package/dist/init/ingest-docs.d.ts +30 -47
- package/dist/init/ingest-docs.js +113 -406
- package/dist/init/ingest-docs.js.map +1 -1
- package/dist/init/init.d.ts +8 -0
- package/dist/init/init.js +58 -29
- package/dist/init/init.js.map +1 -1
- package/dist/init/mapper-parallel.js +1 -1
- package/dist/init/mapper-parallel.js.map +1 -1
- package/dist/init/phases/5-brand.js +1 -1
- package/dist/init/phases/5-brand.js.map +1 -1
- package/dist/init/phases/5b-topic-index.d.ts +30 -0
- package/dist/init/phases/5b-topic-index.js +62 -0
- package/dist/init/phases/5b-topic-index.js.map +1 -0
- package/dist/init/phases/6-docs-ingest.d.ts +4 -5
- package/dist/init/phases/6-docs-ingest.js +5 -6
- package/dist/init/phases/6-docs-ingest.js.map +1 -1
- package/dist/init/phases/index.d.ts +2 -0
- package/dist/init/phases/index.js +1 -0
- package/dist/init/phases/index.js.map +1 -1
- package/dist/init/phases/parallel-678.d.ts +14 -17
- package/dist/init/phases/parallel-678.js +77 -98
- package/dist/init/phases/parallel-678.js.map +1 -1
- package/dist/init/phases/source-comments-output-io.d.ts +16 -10
- package/dist/init/phases/source-comments-output-io.js +7 -10
- package/dist/init/phases/source-comments-output-io.js.map +1 -1
- package/dist/init/phases/types.d.ts +1 -1
- package/dist/init/phases/types.js +1 -0
- package/dist/init/phases/types.js.map +1 -1
- package/dist/init/rules-merge/discover.d.ts +8 -3
- package/dist/init/rules-merge/discover.js +7 -3
- package/dist/init/rules-merge/discover.js.map +1 -1
- package/dist/init/rules-merge/ingest.d.ts +81 -28
- package/dist/init/rules-merge/ingest.js +456 -155
- package/dist/init/rules-merge/ingest.js.map +1 -1
- package/dist/init/sot-emit.d.ts +84 -0
- package/dist/init/sot-emit.js +218 -0
- package/dist/init/sot-emit.js.map +1 -0
- package/dist/init/source-comments/classify.d.ts +12 -10
- package/dist/init/source-comments/classify.js +13 -25
- package/dist/init/source-comments/classify.js.map +1 -1
- package/dist/init/source-comments/index.d.ts +1 -1
- package/dist/init/source-comments/index.js +1 -1
- package/dist/init/source-comments/index.js.map +1 -1
- package/dist/init/source-comments/ingest.d.ts +91 -67
- package/dist/init/source-comments/ingest.js +392 -349
- package/dist/init/source-comments/ingest.js.map +1 -1
- package/dist/init/topic-index/index.d.ts +36 -0
- package/dist/init/topic-index/index.js +46 -0
- package/dist/init/topic-index/index.js.map +1 -0
- package/dist/init/topic-index/judge.d.ts +20 -0
- package/dist/init/topic-index/judge.js +65 -0
- package/dist/init/topic-index/judge.js.map +1 -0
- package/dist/init/topic-index/resolve.d.ts +50 -0
- package/dist/init/topic-index/resolve.js +196 -0
- package/dist/init/topic-index/resolve.js.map +1 -0
- package/dist/init/topic-index/walk.d.ts +43 -0
- package/dist/init/topic-index/walk.js +293 -0
- package/dist/init/topic-index/walk.js.map +1 -0
- package/dist/mcp/history/summarizer.js +1 -1
- package/dist/mcp/history/summarizer.js.map +1 -1
- package/dist/mcp/schemas.d.ts +46 -9
- package/dist/mcp/schemas.js +48 -12
- package/dist/mcp/schemas.js.map +1 -1
- package/dist/mcp/tools/align-drain.d.ts +7 -0
- package/dist/mcp/tools/align-drain.js +26 -0
- package/dist/mcp/tools/align-drain.js.map +1 -0
- package/dist/mcp/tools/archive.js +1 -1
- package/dist/mcp/tools/archive.js.map +1 -1
- package/dist/mcp/tools/attention-restore.js +1 -1
- package/dist/mcp/tools/attention-restore.js.map +1 -1
- package/dist/mcp/tools/attention-serve.d.ts +23 -0
- package/dist/mcp/tools/attention-serve.js +78 -0
- package/dist/mcp/tools/attention-serve.js.map +1 -0
- package/dist/mcp/tools/attention-wait.d.ts +18 -0
- package/dist/mcp/tools/attention-wait.js +74 -0
- package/dist/mcp/tools/attention-wait.js.map +1 -0
- package/dist/mcp/tools/index.js +7 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/init-phases.js +4 -1
- package/dist/mcp/tools/init-phases.js.map +1 -1
- package/dist/mcp/tools/record-decision.js +14 -2
- package/dist/mcp/tools/record-decision.js.map +1 -1
- package/dist/mcp/tools/resolve-attention.d.ts +2 -2
- package/dist/mcp/tools/resolve-attention.js +830 -7
- package/dist/mcp/tools/resolve-attention.js.map +1 -1
- package/dist/status-line/event-queue.d.ts +40 -0
- package/dist/status-line/event-queue.js +195 -0
- package/dist/status-line/event-queue.js.map +1 -0
- package/dist/status-line/format.d.ts +1 -1
- package/dist/status-line/format.js +49 -6
- package/dist/status-line/format.js.map +1 -1
- package/dist/status-line/index.d.ts +41 -0
- package/dist/status-line/index.js +14 -0
- package/dist/status-line/index.js.map +1 -1
- package/dist/status-line/reader.js +23 -18
- package/dist/status-line/reader.js.map +1 -1
- package/dist/status-line/writer.d.ts +1 -1
- package/dist/status-line/writer.js +5 -0
- package/dist/status-line/writer.js.map +1 -1
- package/dist/text/jaccard.d.ts +19 -0
- package/dist/text/jaccard.js +68 -0
- package/dist/text/jaccard.js.map +1 -0
- package/package.json +1 -1
- package/templates/.cairn/git-hooks/pre-commit +16 -3
- package/templates/attention-ui/app.css +406 -0
- package/templates/attention-ui/app.js +384 -0
- package/templates/attention-ui/index.html +56 -0
|
@@ -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":"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"}
|
|
@@ -1,38 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Content-addressed id derivation for decisions and invariants.
|
|
3
3
|
*
|
|
4
|
-
* Decisions:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
61
|
+
export declare function computeDecisionId(input: DecisionIdInput, existing?: Set<string>): string;
|
|
22
62
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
63
|
+
* Compute a stable `INV-<hash>` id from the canonical input. Same
|
|
64
|
+
* collision-extension behavior as `computeDecisionId`.
|
|
25
65
|
*/
|
|
26
|
-
export declare function
|
|
66
|
+
export declare function computeInvariantId(input: InvariantIdInput, existing?: Set<string>): string;
|
|
27
67
|
/**
|
|
28
|
-
* Scan
|
|
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
|
|
71
|
+
export declare function scanExistingDecisionIds(repoRoot: string): Set<string>;
|
|
32
72
|
/**
|
|
33
|
-
*
|
|
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
|
|
75
|
+
export declare function scanExistingInvariantIds(repoRoot: string): Set<string>;
|
|
@@ -1,29 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Content-addressed id derivation for decisions and invariants.
|
|
3
3
|
*
|
|
4
|
-
* Decisions:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
|
26
|
-
* DEC
|
|
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]
|
|
104
|
+
ids.add(`DEC-${match[1]}`);
|
|
47
105
|
}
|
|
48
106
|
}
|
|
49
107
|
return ids;
|
|
50
108
|
}
|
|
51
109
|
/**
|
|
52
|
-
*
|
|
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]
|
|
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":"AAAA
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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,
|
|
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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer C — SessionStart drain (plan §4.3).
|
|
3
|
+
*
|
|
4
|
+
* Reads the rich deferred logs written by Layer A
|
|
5
|
+
* (`.cairn/staleness/layer-a-deferred.jsonl`) and Layer B
|
|
6
|
+
* (`.cairn/staleness/pre-commit-deferred.jsonl`), re-checks each entry
|
|
7
|
+
* against the current source location, and applies one of three
|
|
8
|
+
* verdicts to each surviving entry:
|
|
9
|
+
*
|
|
10
|
+
* - `same` → strip-replace the prose block with `// §DEC-<id>`
|
|
11
|
+
* cite. Pure deterministic for Layer B `tier1`
|
|
12
|
+
* entries (the pre-commit hook already passed the
|
|
13
|
+
* Tier 1 floors); Haiku-judged for everything else.
|
|
14
|
+
* - `different` → drop the entry, no source change.
|
|
15
|
+
* - `ambiguous` → write to `.cairn/ground/alignment-pending/` so
|
|
16
|
+
* the cairn-attention skill surfaces a side-by-side
|
|
17
|
+
* review next session.
|
|
18
|
+
*
|
|
19
|
+
* Drain truncates both deferred logs after running. The lightweight
|
|
20
|
+
* drift events in `.cairn/staleness/log.jsonl` are an audit trail and
|
|
21
|
+
* stay.
|
|
22
|
+
*
|
|
23
|
+
* Cost: capped at `max_haiku_calls` (default 30 per plan §4.3 budget).
|
|
24
|
+
* Excess entries stay in the deferred logs for the next drain. Each
|
|
25
|
+
* Haiku call is verdict-cached at
|
|
26
|
+
* `.cairn/cache/haiku/drain-judge/<blockHash>-<decId>.json` keyed on
|
|
27
|
+
* `(block_content_hash, dec_body_hash)`, so re-running the same block
|
|
28
|
+
* against the same DEC body short-circuits without burning a call.
|
|
29
|
+
*
|
|
30
|
+
* Haiku unavailable fallback: drain attempts the deterministic re-check
|
|
31
|
+
* pass only (Layer B tier1 entries get applied; everything else stays
|
|
32
|
+
* deferred). `setHaikuAvailable(false)` raises the statusline banner.
|
|
33
|
+
*/
|
|
34
|
+
export type DrainJudgeVerdict = "same" | "different" | "ambiguous";
|
|
35
|
+
export interface DrainArgs {
|
|
36
|
+
repoRoot: string;
|
|
37
|
+
/** When provided, drain pushes drain-progress / drain-done blips to this session's queue. */
|
|
38
|
+
sessionId?: string | null;
|
|
39
|
+
/** Hard cap on Haiku judge calls. Default 30 (plan §4.3). */
|
|
40
|
+
maxHaikuCalls?: number;
|
|
41
|
+
/** Dry run — classify but do not strip-replace, write alignment-pending, or truncate logs. */
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Inject the dedup judge — bypasses the live Haiku call. Used by
|
|
45
|
+
* smoke fixtures and the `cairn align drain --mock` debug path.
|
|
46
|
+
*/
|
|
47
|
+
mockJudge?: (args: {
|
|
48
|
+
blockBody: string;
|
|
49
|
+
candidate: {
|
|
50
|
+
id: string;
|
|
51
|
+
body: string;
|
|
52
|
+
};
|
|
53
|
+
}) => Promise<DrainJudgeVerdict>;
|
|
54
|
+
/** Override Haiku availability detection (smoke fixtures). */
|
|
55
|
+
haikuAvailable?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface DrainResult {
|
|
58
|
+
/** Total entries read from both deferred logs. */
|
|
59
|
+
totalEntries: number;
|
|
60
|
+
/** Entries whose source block could not be relocated (gone / edited / cited). */
|
|
61
|
+
droppedMissing: number;
|
|
62
|
+
/** Entries auto-cited via deterministic re-check (Layer B tier1). */
|
|
63
|
+
citedDeterministic: number;
|
|
64
|
+
/** Entries auto-cited via Haiku `same` verdict. */
|
|
65
|
+
citedHaiku: number;
|
|
66
|
+
/** Entries dropped via Haiku `different` verdict. */
|
|
67
|
+
droppedDifferent: number;
|
|
68
|
+
/** Entries written to alignment-pending via Haiku `ambiguous` verdict. */
|
|
69
|
+
pending: number;
|
|
70
|
+
/** Entries left in the deferred logs because the Haiku cap was hit or Haiku is offline. */
|
|
71
|
+
deferred: number;
|
|
72
|
+
/** Total Haiku calls actually issued (cache hits do not count). */
|
|
73
|
+
haikuCalls: number;
|
|
74
|
+
/** True when the drain ran without Haiku (fallback path). */
|
|
75
|
+
haikuFallback: boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare function runDrain(args: DrainArgs): Promise<DrainResult>;
|