@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
|
@@ -1,34 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phase 7b orchestrator — walker → classifier →
|
|
2
|
+
* Phase 7b orchestrator (v0.5.0 SoT model) — walker → classifier →
|
|
3
|
+
* topic-index lookup → emit-or-cite → strip-replace.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
5
|
+
* Plan §5.3 algorithm:
|
|
6
|
+
* 1. Walk source files for prose-bearing comments (existing logic).
|
|
7
|
+
* 2. Classify via Haiku, kind only — `rationale` / `constraint` /
|
|
8
|
+
* `citation` / `license` / `other`. No paraphrased title, no rewritten
|
|
9
|
+
* invariant body, no canonical-topic suggestion.
|
|
10
|
+
* 3. Build a content-fingerprint slug for every rationale + constraint
|
|
11
|
+
* block. Look it up in the existing topic-index:
|
|
12
|
+
* a. **Cite-existing** — slug already owned by a docs/CLAUDE.md/
|
|
13
|
+
* AGENTS.md/rule entry that has been emitted. Strip-replace
|
|
14
|
+
* inserts `// §DEC-<existing>` (or `§INV-<existing>`) at the
|
|
15
|
+
* comment's offset. No new ground-state file is written.
|
|
16
|
+
* b. **Novel** — slug not in the topic-index, or owned by an
|
|
17
|
+
* un-emitted entry. Add the block to the topic-index as a
|
|
18
|
+
* new source-comment SoT entry; emit a verbatim DEC/INV via
|
|
19
|
+
* `emitFromTopicIndex` with `sot_kind: "ledger"`. After emit,
|
|
20
|
+
* strip-replace inserts `§DEC-<new>` / `§INV-<new>`.
|
|
21
|
+
* 4. Auto-promote — every newly-emitted entity ships with
|
|
22
|
+
* `status: accepted` (no `_inbox/` draft queue). Plan §1's pivot:
|
|
23
|
+
* the inbox-as-blocker was the bug; verbatim bodies + auto-promote
|
|
24
|
+
* remove the manual review step.
|
|
25
|
+
* 5. License + citation + other classifications → no-op (no DEC, no
|
|
26
|
+
* strip-replace). License blocks stay verbatim in source.
|
|
27
|
+
*
|
|
28
|
+
* Output side-effects (all relative to repoRoot):
|
|
29
|
+
* - `.cairn/ground/decisions/<DEC-id>.md` (one per novel rationale)
|
|
30
|
+
* - `.cairn/ground/invariants/<INV-id>.md` (one per novel constraint)
|
|
31
|
+
* - `.cairn/ground/topic-index.yaml` (extended with source-comment SoT entries)
|
|
32
|
+
* - `.cairn/ground/anchor-map.yaml` (one anchor per novel slug)
|
|
33
|
+
* - `.cairn/ground/sot-bindings.yaml` (forward+reverse for new ids)
|
|
34
|
+
* - `.cairn/ground/sot-cache.yaml` (token cache for Layer A)
|
|
35
|
+
* - `.cairn/ground/scope-index.yaml` (file → ids that landed in source)
|
|
36
|
+
* - `.cairn/baseline/source-comments-<ISO>.yaml` (full audit — every block + verdict)
|
|
37
|
+
* - source files (stripped & cited per replacement)
|
|
18
38
|
*/
|
|
19
39
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
20
40
|
import { dirname, join } from "node:path";
|
|
21
41
|
import { stringify as stringifyYaml } from "yaml";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { coerceInvariantIds, readScopeIndex, writeScopeIndex, } from "../../ground/scope-index.js";
|
|
42
|
+
import { writeDecisionsLedger, writeInvariantsLedger, } from "../../ground/ledgers.js";
|
|
43
|
+
import { bodyContentHash, deriveLedgerDecId, deriveLedgerInvId, emptyAnchorMap, emptySotBindings, emptySotCache, emptyTopicIndex, readAnchorMap, readSotBindings, readSotCache, readTopicIndex, setAnchor, setSotCacheEntry, setTopic, topicSlug, writeAnchorMap, writeSotBindings, writeSotCache, writeTopicIndex, } from "../../ground/index.js";
|
|
44
|
+
import { coerceDecisionIds, coerceInvariantIds, readScopeIndex, writeScopeIndex, } from "../../ground/scope-index.js";
|
|
26
45
|
import { logger } from "../../logger.js";
|
|
27
|
-
import {
|
|
46
|
+
import { emitFromTopicIndex } from "../sot-emit.js";
|
|
28
47
|
import { applyStripReplace, formatBareCitation, } from "./strip-replace.js";
|
|
29
48
|
import { classifyBlocks } from "./classify.js";
|
|
30
49
|
import { walkSourceComments } from "./walker.js";
|
|
31
50
|
const log = logger("init.source-comments.ingest");
|
|
51
|
+
const CAPTURE_SOURCE = "init-source-comments";
|
|
32
52
|
/* -------------------------------------------------------------------------- */
|
|
33
53
|
/* Public */
|
|
34
54
|
/* -------------------------------------------------------------------------- */
|
|
@@ -36,6 +56,7 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
36
56
|
const repoRoot = args.repoRoot;
|
|
37
57
|
const nowIso = args.nowIso ?? new Date().toISOString();
|
|
38
58
|
const tsSlug = nowIso.replace(/[:.]/g, "-").slice(0, 19);
|
|
59
|
+
// ── 1. Walk ──────────────────────────────────────────────────────
|
|
39
60
|
const walkOpts = { repoRoot };
|
|
40
61
|
if (args.walkOptions?.fileCap !== undefined) {
|
|
41
62
|
walkOpts.fileCap = args.walkOptions.fileCap;
|
|
@@ -44,6 +65,7 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
44
65
|
walkOpts.onlyFiles = args.walkOptions.onlyFiles;
|
|
45
66
|
}
|
|
46
67
|
const walk = walkSourceComments(walkOpts);
|
|
68
|
+
// ── 2. Classify (kind only) ─────────────────────────────────────
|
|
47
69
|
const classifyResult = await classifyBlocks({
|
|
48
70
|
blocks: walk.blocks,
|
|
49
71
|
repoRoot,
|
|
@@ -64,129 +86,250 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
64
86
|
continue;
|
|
65
87
|
kindCounts[c.kind] = (kindCounts[c.kind] ?? 0) + 1;
|
|
66
88
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
for (let i = 0; i < walk.blocks.length; i++) {
|
|
89
|
+
// ── 3. Topic-index lookup + extension ────────────────────────────
|
|
90
|
+
let topicIndex = readTopicIndex(repoRoot);
|
|
91
|
+
if (Object.keys(topicIndex.topics).length === 0)
|
|
92
|
+
topicIndex = emptyTopicIndex();
|
|
93
|
+
let anchorMap = readAnchorMap(repoRoot);
|
|
94
|
+
if (Object.keys(anchorMap.anchors).length === 0)
|
|
95
|
+
anchorMap = emptyAnchorMap();
|
|
96
|
+
const resolutionByBlockId = new Map();
|
|
97
|
+
const skipped = [];
|
|
98
|
+
const emitKindBySlug = new Map();
|
|
99
|
+
for (let i = 0; i < walk.blocks.length; i += 1) {
|
|
79
100
|
const block = walk.blocks[i];
|
|
80
101
|
const cls = classifyResult.classifications[i];
|
|
81
102
|
if (block === undefined || cls === undefined)
|
|
82
103
|
continue;
|
|
83
|
-
if (cls.kind
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const confidence = args.globs !== undefined
|
|
87
|
-
? scoreDecDraft({
|
|
88
|
-
sourceFile: block.file,
|
|
89
|
-
prose: block.prose,
|
|
90
|
-
title: cls.suggestedDecDraft,
|
|
91
|
-
rawComment: block.raw,
|
|
92
|
-
globs: args.globs,
|
|
93
|
-
...(args.pilotModule !== undefined ? { pilotModule: args.pilotModule } : {}),
|
|
94
|
-
})
|
|
95
|
-
: undefined;
|
|
96
|
-
if (args.dryRun !== true) {
|
|
97
|
-
const written = writeDecDraft({
|
|
98
|
-
repoRoot,
|
|
99
|
-
id,
|
|
100
|
-
block,
|
|
101
|
-
classification: cls,
|
|
102
|
-
generatedAt: nowIso,
|
|
103
|
-
...(confidence !== undefined ? { confidence } : {}),
|
|
104
|
-
});
|
|
105
|
-
decDraftsWritten.push({
|
|
106
|
-
id,
|
|
107
|
-
path: written.relPath,
|
|
108
|
-
sourceFile: block.file,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
decDraftsWritten.push({
|
|
113
|
-
id,
|
|
114
|
-
path: `.cairn/ground/decisions/_inbox/${id}.draft.md`,
|
|
115
|
-
sourceFile: block.file,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
104
|
+
if (cls.kind !== "rationale" && cls.kind !== "constraint") {
|
|
105
|
+
skipped.push({ blockId: block.id, reason: `kind=${cls.kind}` });
|
|
106
|
+
continue;
|
|
118
107
|
}
|
|
119
|
-
if (cls.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
start_line: block.startLine,
|
|
124
|
-
end_line: block.endLine,
|
|
125
|
-
proposed: cls.suggestedInvariant,
|
|
126
|
-
canonical_topic: cls.suggestedCanonicalTopic,
|
|
108
|
+
if (cls.failed) {
|
|
109
|
+
skipped.push({
|
|
110
|
+
blockId: block.id,
|
|
111
|
+
reason: `classifier failed: ${cls.errorMessage ?? "unknown"}`,
|
|
127
112
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const slug = topicSlug(block.prose);
|
|
116
|
+
const existing = topicIndex.topics[slug];
|
|
117
|
+
if (existing !== undefined && existing.dec_id !== undefined) {
|
|
118
|
+
// Cite-existing — another source already owns this topic + emitted.
|
|
119
|
+
resolutionByBlockId.set(block.id, {
|
|
120
|
+
kind: "cite",
|
|
121
|
+
existingId: existing.dec_id,
|
|
122
|
+
slug,
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const emitKind = cls.kind === "constraint" ? "constraint" : "decision";
|
|
127
|
+
if (existing !== undefined) {
|
|
128
|
+
// Slug already in topic-index but not yet emitted (e.g. phase 5b
|
|
129
|
+
// walked it as some other kind). Keep the existing entry, just
|
|
130
|
+
// remember the emit kind so the classifier callback below maps
|
|
131
|
+
// the entry to the right phase-7b verdict.
|
|
132
|
+
emitKindBySlug.set(slug, emitKind);
|
|
133
|
+
resolutionByBlockId.set(block.id, { kind: "emit", slug, emitKind });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Novel topic — register a fresh source-comment entry.
|
|
137
|
+
const lineRange = [block.startLine, block.endLine];
|
|
138
|
+
const newEntry = {
|
|
139
|
+
slug,
|
|
140
|
+
sot_source: block.file,
|
|
141
|
+
candidates: [
|
|
142
|
+
{
|
|
143
|
+
file: block.file,
|
|
144
|
+
kind: "source-comment",
|
|
145
|
+
line_range: lineRange,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
created_at: nowIso,
|
|
149
|
+
};
|
|
150
|
+
topicIndex = setTopic(topicIndex, slug, newEntry);
|
|
151
|
+
anchorMap = setAnchor(anchorMap, slug, {
|
|
152
|
+
file: block.file,
|
|
153
|
+
content_hash: bodyContentHash(block.prose),
|
|
154
|
+
line_range: lineRange,
|
|
155
|
+
kind: "source-comment",
|
|
156
|
+
});
|
|
157
|
+
emitKindBySlug.set(slug, emitKind);
|
|
158
|
+
resolutionByBlockId.set(block.id, { kind: "emit", slug, emitKind });
|
|
159
|
+
}
|
|
160
|
+
// ── 4. Emit (sot-emit, sot_kind=ledger) ──────────────────────────
|
|
161
|
+
const emit = await emitFromTopicIndex({
|
|
162
|
+
repoRoot,
|
|
163
|
+
topicIndex,
|
|
164
|
+
anchorMap,
|
|
165
|
+
filter: (entry) => entry.dec_id === undefined &&
|
|
166
|
+
isSourceCommentEntry(entry) &&
|
|
167
|
+
emitKindBySlug.has(entry.slug),
|
|
168
|
+
classifier: async ({ entry }) => {
|
|
169
|
+
const k = emitKindBySlug.get(entry.slug);
|
|
170
|
+
if (k === undefined)
|
|
171
|
+
return { kind: "skip", title: "" };
|
|
172
|
+
return { kind: k === "constraint" ? "constraint" : "decision", title: "" };
|
|
173
|
+
},
|
|
174
|
+
sot_kind: "ledger",
|
|
175
|
+
capture_source: CAPTURE_SOURCE,
|
|
176
|
+
idDeriver: ({ entry, kind }) => {
|
|
177
|
+
const sot = entry.candidates.find((c) => c.file === entry.sot_source);
|
|
178
|
+
const range = sot?.line_range;
|
|
179
|
+
const offset = range !== undefined ? range[0] : 0;
|
|
180
|
+
const inputs = {
|
|
181
|
+
source_file: entry.sot_source,
|
|
182
|
+
source_offset: offset,
|
|
183
|
+
capture_source: CAPTURE_SOURCE,
|
|
184
|
+
};
|
|
185
|
+
return kind === "constraint"
|
|
186
|
+
? deriveLedgerInvId(inputs)
|
|
187
|
+
: deriveLedgerDecId(inputs);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
topicIndex = emit.topicIndex;
|
|
191
|
+
if (args.dryRun !== true) {
|
|
192
|
+
persistGroundState({
|
|
193
|
+
repoRoot,
|
|
194
|
+
topicIndex,
|
|
195
|
+
anchorMap,
|
|
196
|
+
bindings: emit.bindings,
|
|
197
|
+
cache: emit.cache,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// ── 5. Build emit / cite records keyed by slug ──────────────────
|
|
201
|
+
const decsWritten = [];
|
|
202
|
+
const invsWritten = [];
|
|
203
|
+
const emittedIdBySlug = new Map();
|
|
204
|
+
for (const rec of emit.emitted) {
|
|
205
|
+
emittedIdBySlug.set(rec.slug, rec.id);
|
|
206
|
+
const target = {
|
|
207
|
+
id: rec.id,
|
|
208
|
+
path: rec.kind === "DEC"
|
|
209
|
+
? `.cairn/ground/decisions/${rec.id}.md`
|
|
210
|
+
: `.cairn/ground/invariants/${rec.id}.md`,
|
|
211
|
+
sourceFile: rec.source_file,
|
|
212
|
+
slug: rec.slug,
|
|
213
|
+
status: "accepted",
|
|
214
|
+
};
|
|
215
|
+
if (rec.kind === "DEC")
|
|
216
|
+
decsWritten.push(target);
|
|
217
|
+
else
|
|
218
|
+
invsWritten.push(target);
|
|
219
|
+
}
|
|
220
|
+
for (const sk of emit.skipped) {
|
|
221
|
+
skipped.push({ blockId: `slug:${sk.slug}`, reason: sk.reason });
|
|
222
|
+
}
|
|
223
|
+
// ── 6. Strip-replace ─────────────────────────────────────────────
|
|
224
|
+
const citesEmitted = [];
|
|
225
|
+
const stripItems = [];
|
|
226
|
+
for (let i = 0; i < walk.blocks.length; i += 1) {
|
|
227
|
+
const block = walk.blocks[i];
|
|
228
|
+
if (block === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const resolution = resolutionByBlockId.get(block.id);
|
|
231
|
+
if (resolution === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const targetId = resolution.kind === "cite"
|
|
234
|
+
? resolution.existingId
|
|
235
|
+
: emittedIdBySlug.get(resolution.slug);
|
|
236
|
+
if (targetId === undefined) {
|
|
237
|
+
skipped.push({
|
|
167
238
|
blockId: block.id,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
239
|
+
reason: `emit produced no id for slug ${resolution.slug}`,
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (resolution.kind === "cite") {
|
|
244
|
+
citesEmitted.push({
|
|
245
|
+
id: targetId,
|
|
246
|
+
sourceFile: block.file,
|
|
247
|
+
lineRange: [block.startLine, block.endLine],
|
|
248
|
+
slug: resolution.slug,
|
|
173
249
|
});
|
|
174
250
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
251
|
+
stripItems.push({
|
|
252
|
+
blockId: block.id,
|
|
253
|
+
file: block.file,
|
|
254
|
+
startOffset: block.startOffset,
|
|
255
|
+
endOffset: block.endOffset,
|
|
256
|
+
replacement: formatBareCitation(block.lang, targetId),
|
|
257
|
+
expectedRaw: block.raw,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
let stripFilesModified = 0;
|
|
261
|
+
let stripItemsApplied = 0;
|
|
262
|
+
let stripItemsSkipped = 0;
|
|
263
|
+
let stripOutcomes = [];
|
|
264
|
+
let stripError = null;
|
|
265
|
+
if (args.dryRun !== true && stripItems.length > 0) {
|
|
266
|
+
log.info({
|
|
267
|
+
items: stripItems.length,
|
|
268
|
+
files: [...new Set(stripItems.map((it) => it.file))],
|
|
269
|
+
}, "strip-replace: starting");
|
|
270
|
+
try {
|
|
271
|
+
const dirtyDecisions = {};
|
|
272
|
+
for (const item of stripItems)
|
|
273
|
+
dirtyDecisions[item.file] = "overwrite";
|
|
274
|
+
const result = applyStripReplace({
|
|
275
|
+
repoRoot,
|
|
276
|
+
items: stripItems,
|
|
277
|
+
dirtyDecisions,
|
|
183
278
|
});
|
|
279
|
+
stripFilesModified = result.filesModified;
|
|
280
|
+
stripItemsApplied = result.itemsApplied;
|
|
281
|
+
stripItemsSkipped = result.itemsSkipped;
|
|
282
|
+
stripOutcomes = result.files.map((o) => ({
|
|
283
|
+
file: o.file,
|
|
284
|
+
applied: o.itemsApplied,
|
|
285
|
+
skipped: o.itemsSkipped.map((s) => ({
|
|
286
|
+
blockId: s.blockId,
|
|
287
|
+
reason: s.reason,
|
|
288
|
+
})),
|
|
289
|
+
fileSkipReason: o.fileSkipReason ?? null,
|
|
290
|
+
}));
|
|
291
|
+
log.info({
|
|
292
|
+
filesModified: result.filesModified,
|
|
293
|
+
itemsApplied: result.itemsApplied,
|
|
294
|
+
itemsSkipped: result.itemsSkipped,
|
|
295
|
+
}, "strip-replace: complete");
|
|
296
|
+
try {
|
|
297
|
+
updateScopeIndexFromStripItems(repoRoot, stripItems);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "scope-index update from strip items failed");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
stripError = err instanceof Error ? err.message : String(err);
|
|
305
|
+
log.warn({ err: stripError }, "strip-replace failed");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (stripItems.length === 0) {
|
|
309
|
+
log.info("strip-replace: no items (no rationale/constraint blocks classified)");
|
|
310
|
+
}
|
|
311
|
+
// ── 7. Ledger rebuilds ───────────────────────────────────────────
|
|
312
|
+
if (args.dryRun !== true) {
|
|
313
|
+
if (invsWritten.length > 0) {
|
|
314
|
+
try {
|
|
315
|
+
writeInvariantsLedger({ repoRoot });
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "invariants ledger rebuild failed");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (decsWritten.length > 0) {
|
|
322
|
+
try {
|
|
323
|
+
writeDecisionsLedger({ repoRoot });
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "decisions ledger rebuild failed");
|
|
327
|
+
}
|
|
184
328
|
}
|
|
185
329
|
}
|
|
330
|
+
// ── 8. Audit yaml ───────────────────────────────────────────────
|
|
186
331
|
const auditRelPath = `.cairn/baseline/source-comments-${tsSlug}.yaml`;
|
|
187
332
|
const auditPath = join(repoRoot, auditRelPath);
|
|
188
|
-
let invariantProposalsPath = null;
|
|
189
|
-
let canonicalCitationsPath = null;
|
|
190
333
|
if (args.dryRun !== true) {
|
|
191
334
|
writeYaml(auditPath, {
|
|
192
335
|
run_at: nowIso,
|
|
@@ -201,6 +344,9 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
201
344
|
batches_failed: classifyResult.batchesFailed,
|
|
202
345
|
input_tokens: classifyResult.inputTokens,
|
|
203
346
|
output_tokens: classifyResult.outputTokens,
|
|
347
|
+
decs_written: decsWritten.length,
|
|
348
|
+
invs_written: invsWritten.length,
|
|
349
|
+
cites_emitted: citesEmitted.length,
|
|
204
350
|
blocks: walk.blocks.map((b, idx) => ({
|
|
205
351
|
block_id: b.id,
|
|
206
352
|
file: b.file,
|
|
@@ -215,123 +361,36 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
215
361
|
end_offset: b.endOffset,
|
|
216
362
|
raw: b.raw,
|
|
217
363
|
classification: classifyResult.classifications[idx] ?? null,
|
|
364
|
+
resolution: serializeResolution(resolutionByBlockId.get(b.id)),
|
|
218
365
|
})),
|
|
219
366
|
});
|
|
220
|
-
if (invariantProposals.length > 0) {
|
|
221
|
-
const rel = `.cairn/baseline/invariant-proposals-${tsSlug}.yaml`;
|
|
222
|
-
invariantProposalsPath = join(repoRoot, rel);
|
|
223
|
-
writeYaml(invariantProposalsPath, {
|
|
224
|
-
run_at: nowIso,
|
|
225
|
-
proposals: invariantProposals,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
if (canonicalCitations.length > 0) {
|
|
229
|
-
const rel = `.cairn/baseline/canonical-citations-${tsSlug}.yaml`;
|
|
230
|
-
canonicalCitationsPath = join(repoRoot, rel);
|
|
231
|
-
writeYaml(canonicalCitationsPath, {
|
|
232
|
-
run_at: nowIso,
|
|
233
|
-
citations: canonicalCitations,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
367
|
}
|
|
237
368
|
log.info({
|
|
238
369
|
files: walk.files.length,
|
|
239
370
|
blocks: walk.blocks.length,
|
|
240
371
|
kindCounts,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
372
|
+
decs: decsWritten.length,
|
|
373
|
+
invs: invsWritten.length,
|
|
374
|
+
cites: citesEmitted.length,
|
|
375
|
+
stripApplied: stripItemsApplied,
|
|
376
|
+
stripSkipped: stripItemsSkipped,
|
|
244
377
|
inputTokens: classifyResult.inputTokens,
|
|
245
378
|
outputTokens: classifyResult.outputTokens,
|
|
246
379
|
}, "source-comments ingestion complete");
|
|
247
|
-
// Rebuild the invariants ledger after writing new INV-<NNNN>.md files so
|
|
248
|
-
// that §INV-NNNN tokens are resolvable before the strip-replace stage writes
|
|
249
|
-
// bare citations into source. Lens + MCP read tools resolve through the
|
|
250
|
-
// ledger, not by re-walking the dir.
|
|
251
|
-
if (args.dryRun !== true && invariantsWritten.length > 0) {
|
|
252
|
-
try {
|
|
253
|
-
writeInvariantsLedger({ repoRoot });
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
log.warn({ err: err instanceof Error ? err.message : String(err) }, "invariants ledger rebuild failed");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Strip the original constraint comment from each source file and
|
|
260
|
-
// replace with a bare `// §INV-NNNN` cite. Adoption assumes a clean
|
|
261
|
-
// working tree (init phase 1's preflight); pass `overwrite` for
|
|
262
|
-
// every file so dirty-skip doesn't bite us — the operator
|
|
263
|
-
// consented to source mutation when they consented to adoption.
|
|
264
|
-
let invariantStripFilesModified = 0;
|
|
265
|
-
let invariantStripItemsApplied = 0;
|
|
266
|
-
let invariantStripItemsSkipped = 0;
|
|
267
|
-
let invariantStripOutcomes = [];
|
|
268
|
-
let invariantStripError = null;
|
|
269
|
-
if (args.dryRun !== true && invariantStripItems.length > 0) {
|
|
270
|
-
log.info({
|
|
271
|
-
items: invariantStripItems.length,
|
|
272
|
-
files: [...new Set(invariantStripItems.map((it) => it.file))],
|
|
273
|
-
}, "invariant strip-replace: starting");
|
|
274
|
-
try {
|
|
275
|
-
const dirtyDecisions = {};
|
|
276
|
-
for (const item of invariantStripItems)
|
|
277
|
-
dirtyDecisions[item.file] = "overwrite";
|
|
278
|
-
const result = applyStripReplace({
|
|
279
|
-
repoRoot,
|
|
280
|
-
items: invariantStripItems,
|
|
281
|
-
dirtyDecisions,
|
|
282
|
-
});
|
|
283
|
-
invariantStripFilesModified = result.filesModified;
|
|
284
|
-
invariantStripItemsApplied = result.itemsApplied;
|
|
285
|
-
invariantStripItemsSkipped = result.itemsSkipped;
|
|
286
|
-
invariantStripOutcomes = result.files.map((o) => ({
|
|
287
|
-
file: o.file,
|
|
288
|
-
applied: o.itemsApplied,
|
|
289
|
-
skipped: o.itemsSkipped.map((s) => ({ blockId: s.blockId, reason: s.reason })),
|
|
290
|
-
fileSkipReason: o.fileSkipReason ?? null,
|
|
291
|
-
}));
|
|
292
|
-
log.info({
|
|
293
|
-
filesModified: result.filesModified,
|
|
294
|
-
itemsApplied: result.itemsApplied,
|
|
295
|
-
itemsSkipped: result.itemsSkipped,
|
|
296
|
-
outcomes: invariantStripOutcomes,
|
|
297
|
-
}, "invariant strip-replace: complete");
|
|
298
|
-
// Now that we know which §INV-NNNN landed in which file, populate
|
|
299
|
-
// the scope-index for those files. Phase 3 mapper ran before any
|
|
300
|
-
// invariants existed, so its scope_index entries for these files
|
|
301
|
-
// had empty `invariants: []` arrays. Without this update the
|
|
302
|
-
// read-enricher legend's "Invariants in scope" header stays blank
|
|
303
|
-
// even though the source carries the cite tokens.
|
|
304
|
-
try {
|
|
305
|
-
updateScopeIndexFromStripItems(repoRoot, invariantStripItems);
|
|
306
|
-
}
|
|
307
|
-
catch (err) {
|
|
308
|
-
log.warn({ err: err instanceof Error ? err.message : String(err) }, "scope-index update from strip items failed");
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch (err) {
|
|
312
|
-
invariantStripError = err instanceof Error ? err.message : String(err);
|
|
313
|
-
log.warn({ err: invariantStripError }, "invariant strip-replace failed");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
else if (invariantStripItems.length === 0) {
|
|
317
|
-
log.info("invariant strip-replace: no items (no constraint blocks classified)");
|
|
318
|
-
}
|
|
319
380
|
return {
|
|
320
381
|
walk,
|
|
321
382
|
classifications: classifyResult.classifications,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
383
|
+
decsWritten,
|
|
384
|
+
invsWritten,
|
|
385
|
+
citesEmitted,
|
|
386
|
+
skipped,
|
|
387
|
+
stripFilesModified,
|
|
388
|
+
stripItemsApplied,
|
|
389
|
+
stripItemsSkipped,
|
|
390
|
+
stripOutcomes,
|
|
391
|
+
stripError,
|
|
331
392
|
auditPath,
|
|
332
393
|
auditRelPath,
|
|
333
|
-
invariantProposalsPath,
|
|
334
|
-
canonicalCitationsPath,
|
|
335
394
|
inputTokens: classifyResult.inputTokens,
|
|
336
395
|
outputTokens: classifyResult.outputTokens,
|
|
337
396
|
batchesRun: classifyResult.batchesRun,
|
|
@@ -340,56 +399,121 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
340
399
|
};
|
|
341
400
|
}
|
|
342
401
|
/* -------------------------------------------------------------------------- */
|
|
343
|
-
/*
|
|
402
|
+
/* Helpers */
|
|
344
403
|
/* -------------------------------------------------------------------------- */
|
|
404
|
+
function isSourceCommentEntry(entry) {
|
|
405
|
+
const sot = entry.candidates.find((c) => c.file === entry.sot_source);
|
|
406
|
+
return sot?.kind === "source-comment";
|
|
407
|
+
}
|
|
408
|
+
function serializeResolution(resolution) {
|
|
409
|
+
if (resolution === undefined)
|
|
410
|
+
return null;
|
|
411
|
+
if (resolution.kind === "cite") {
|
|
412
|
+
return { kind: "cite", existing_id: resolution.existingId, slug: resolution.slug };
|
|
413
|
+
}
|
|
414
|
+
return { kind: "emit", slug: resolution.slug, emit_kind: resolution.emitKind };
|
|
415
|
+
}
|
|
416
|
+
function persistGroundState(args) {
|
|
417
|
+
const { repoRoot } = args;
|
|
418
|
+
// Re-read each file right before write so we merge with any other phase
|
|
419
|
+
// that committed concurrently. parallel-678 still uses Promise.allSettled
|
|
420
|
+
// across phases 6/7b/7c; sequential individual phase tools are race-free.
|
|
421
|
+
const freshTopic = readTopicIndex(repoRoot);
|
|
422
|
+
const baseTopic = Object.keys(freshTopic.topics).length > 0 ? freshTopic : emptyTopicIndex();
|
|
423
|
+
for (const [slug, entry] of Object.entries(args.topicIndex.topics)) {
|
|
424
|
+
baseTopic.topics[slug] = entry;
|
|
425
|
+
}
|
|
426
|
+
baseTopic.generated = new Date().toISOString();
|
|
427
|
+
writeTopicIndex(repoRoot, baseTopic);
|
|
428
|
+
const freshAnchor = readAnchorMap(repoRoot);
|
|
429
|
+
const baseAnchor = Object.keys(freshAnchor.anchors).length > 0 ? freshAnchor : emptyAnchorMap();
|
|
430
|
+
for (const [slug, anchor] of Object.entries(args.anchorMap.anchors)) {
|
|
431
|
+
baseAnchor.anchors[slug] = anchor;
|
|
432
|
+
}
|
|
433
|
+
baseAnchor.generated = new Date().toISOString();
|
|
434
|
+
writeAnchorMap(repoRoot, baseAnchor);
|
|
435
|
+
const freshBindings = readSotBindings(repoRoot);
|
|
436
|
+
const baseBindings = Object.keys(freshBindings.forward).length > 0 ? freshBindings : emptySotBindings();
|
|
437
|
+
for (const [decId, sotPath] of Object.entries(args.bindings.forward)) {
|
|
438
|
+
baseBindings.forward[decId] = sotPath;
|
|
439
|
+
}
|
|
440
|
+
for (const [sotPath, decIds] of Object.entries(args.bindings.reverse)) {
|
|
441
|
+
const seen = new Set(baseBindings.reverse[sotPath] ?? []);
|
|
442
|
+
for (const id of decIds)
|
|
443
|
+
seen.add(id);
|
|
444
|
+
baseBindings.reverse[sotPath] = Array.from(seen);
|
|
445
|
+
}
|
|
446
|
+
baseBindings.generated = new Date().toISOString();
|
|
447
|
+
writeSotBindings(repoRoot, baseBindings);
|
|
448
|
+
const freshCache = readSotCache(repoRoot);
|
|
449
|
+
let baseCache = Object.keys(freshCache.entries).length > 0 ? freshCache : emptySotCache();
|
|
450
|
+
for (const [decId, entry] of Object.entries(args.cache.entries)) {
|
|
451
|
+
baseCache = setSotCacheEntry(baseCache, decId, entry);
|
|
452
|
+
}
|
|
453
|
+
baseCache.generated = new Date().toISOString();
|
|
454
|
+
writeSotCache(repoRoot, baseCache);
|
|
455
|
+
}
|
|
345
456
|
/**
|
|
346
|
-
* After the strip-replace pass inserts
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* Each file's existing invariants array gets unioned with the new IDs,
|
|
354
|
-
* de-duplicated, and re-coerced (defense-in-depth). Files absent from
|
|
355
|
-
* the scope-index get a fresh entry. Decisions arrays are left
|
|
356
|
-
* untouched — DEC strips happen at accept-time via cairn-attention,
|
|
357
|
-
* not in this Phase 7b bulk pass.
|
|
457
|
+
* After the strip-replace pass inserts cites into source files, fold those
|
|
458
|
+
* IDs into `.cairn/ground/scope-index.yaml`. Phase 3 mapper that originally
|
|
459
|
+
* seeded scope-index ran before any DECs/INVs existed, so its `decisions:
|
|
460
|
+
* []` / `invariants: []` arrays for these files were correctly empty. Now
|
|
461
|
+
* that the cite tokens are landed, the read-enricher's "in scope" headers
|
|
462
|
+
* should reflect them.
|
|
358
463
|
*/
|
|
359
464
|
function updateScopeIndexFromStripItems(repoRoot, items) {
|
|
360
465
|
if (items.length === 0)
|
|
361
466
|
return;
|
|
362
|
-
const
|
|
363
|
-
const
|
|
467
|
+
const decsByFile = new Map();
|
|
468
|
+
const invsByFile = new Map();
|
|
469
|
+
const decMatch = /§(DEC-[0-9a-f]{7,})/;
|
|
470
|
+
const invMatch = /§(INV-[0-9a-f]{7,})/;
|
|
364
471
|
for (const item of items) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
472
|
+
const decM = item.replacement.match(decMatch);
|
|
473
|
+
const invM = item.replacement.match(invMatch);
|
|
474
|
+
if (decM !== null) {
|
|
475
|
+
const id = decM[1];
|
|
476
|
+
if (id !== undefined) {
|
|
477
|
+
let set = decsByFile.get(item.file);
|
|
478
|
+
if (set === undefined) {
|
|
479
|
+
set = new Set();
|
|
480
|
+
decsByFile.set(item.file, set);
|
|
481
|
+
}
|
|
482
|
+
set.add(id);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (invM !== null) {
|
|
486
|
+
const id = invM[1];
|
|
487
|
+
if (id !== undefined) {
|
|
488
|
+
let set = invsByFile.get(item.file);
|
|
489
|
+
if (set === undefined) {
|
|
490
|
+
set = new Set();
|
|
491
|
+
invsByFile.set(item.file, set);
|
|
492
|
+
}
|
|
493
|
+
set.add(id);
|
|
494
|
+
}
|
|
375
495
|
}
|
|
376
|
-
set.add(id);
|
|
377
496
|
}
|
|
378
|
-
if (
|
|
497
|
+
if (decsByFile.size === 0 && invsByFile.size === 0)
|
|
379
498
|
return;
|
|
380
499
|
const existing = readScopeIndex(repoRoot) ?? {
|
|
381
500
|
generated: new Date().toISOString(),
|
|
382
501
|
files: {},
|
|
383
502
|
};
|
|
384
|
-
|
|
503
|
+
const allFiles = new Set([...decsByFile.keys(), ...invsByFile.keys()]);
|
|
504
|
+
for (const file of allFiles) {
|
|
385
505
|
const prior = existing.files[file];
|
|
386
|
-
const
|
|
506
|
+
const mergedDecs = coerceDecisionIds([
|
|
507
|
+
...(prior?.decisions ?? []),
|
|
508
|
+
...(decsByFile.get(file) ?? []),
|
|
509
|
+
]);
|
|
510
|
+
const mergedInvs = coerceInvariantIds([
|
|
387
511
|
...(prior?.invariants ?? []),
|
|
388
|
-
...
|
|
512
|
+
...(invsByFile.get(file) ?? []),
|
|
389
513
|
]);
|
|
390
514
|
const next = {
|
|
391
|
-
decisions:
|
|
392
|
-
invariants:
|
|
515
|
+
decisions: mergedDecs,
|
|
516
|
+
invariants: mergedInvs,
|
|
393
517
|
};
|
|
394
518
|
if (prior?.unscoped === true)
|
|
395
519
|
next.unscoped = true;
|
|
@@ -400,92 +524,11 @@ function updateScopeIndexFromStripItems(repoRoot, items) {
|
|
|
400
524
|
files: existing.files,
|
|
401
525
|
};
|
|
402
526
|
writeScopeIndex(repoRoot, updated);
|
|
403
|
-
log.info({
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
409
|
-
const filename = `${args.id}.draft.md`;
|
|
410
|
-
const abs = join(inboxDir, filename);
|
|
411
|
-
const rel = `.cairn/ground/decisions/_inbox/${filename}`;
|
|
412
|
-
const fm = {
|
|
413
|
-
id: args.id,
|
|
414
|
-
title: args.classification.suggestedDecDraft || `(untitled — from ${args.block.file})`,
|
|
415
|
-
type: "adr",
|
|
416
|
-
status: "draft-from-source-comment",
|
|
417
|
-
audience: "dual",
|
|
418
|
-
generated: args.generatedAt,
|
|
419
|
-
"verified-at": args.generatedAt,
|
|
420
|
-
decided_at: args.generatedAt,
|
|
421
|
-
decided_by: "cairn-init",
|
|
422
|
-
capture_source: "init-source-comments",
|
|
423
|
-
capture_confidence: args.confidence ?? "medium",
|
|
424
|
-
sourceFile: args.block.file,
|
|
425
|
-
sourceRange: `${args.block.startLine}-${args.block.endLine}`,
|
|
426
|
-
blockId: args.block.id,
|
|
427
|
-
canonicalTopic: args.classification.suggestedCanonicalTopic,
|
|
428
|
-
};
|
|
429
|
-
const lines = [];
|
|
430
|
-
lines.push("---");
|
|
431
|
-
lines.push(stringifyYaml(fm).trimEnd());
|
|
432
|
-
lines.push("---");
|
|
433
|
-
lines.push("");
|
|
434
|
-
lines.push(`# ${args.id} — ${fm["title"]}`);
|
|
435
|
-
lines.push("");
|
|
436
|
-
lines.push("## Source comment");
|
|
437
|
-
lines.push("");
|
|
438
|
-
lines.push("```");
|
|
439
|
-
lines.push(args.block.raw);
|
|
440
|
-
lines.push("```");
|
|
441
|
-
lines.push("");
|
|
442
|
-
lines.push("## Proposed rationale");
|
|
443
|
-
lines.push("");
|
|
444
|
-
lines.push(args.block.prose);
|
|
445
|
-
lines.push("");
|
|
446
|
-
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
447
|
-
return { absPath: abs, relPath: rel };
|
|
448
|
-
}
|
|
449
|
-
function writeInvariantFile(args) {
|
|
450
|
-
const dir = invariantsDir(args.repoRoot);
|
|
451
|
-
mkdirSync(dir, { recursive: true });
|
|
452
|
-
const filename = `${args.id}.md`;
|
|
453
|
-
const abs = join(dir, filename);
|
|
454
|
-
const rel = `.cairn/ground/invariants/${filename}`;
|
|
455
|
-
const fm = {
|
|
456
|
-
id: args.id,
|
|
457
|
-
title: args.classification.suggestedInvariant.split("\n")[0]?.slice(0, 120) ?? args.id,
|
|
458
|
-
type: "invariant",
|
|
459
|
-
status: "active",
|
|
460
|
-
audience: "dual",
|
|
461
|
-
generated: args.generatedAt,
|
|
462
|
-
"verified-at": args.generatedAt,
|
|
463
|
-
capture_confidence: args.confidence ?? "medium",
|
|
464
|
-
source_decision: null,
|
|
465
|
-
sourceFile: args.block.file,
|
|
466
|
-
sourceRange: `${args.block.startLine}-${args.block.endLine}`,
|
|
467
|
-
blockId: args.block.id,
|
|
468
|
-
canonicalTopic: args.classification.suggestedCanonicalTopic,
|
|
469
|
-
};
|
|
470
|
-
const lines = [];
|
|
471
|
-
lines.push("---");
|
|
472
|
-
lines.push(stringifyYaml(fm).trimEnd());
|
|
473
|
-
lines.push("---");
|
|
474
|
-
lines.push("");
|
|
475
|
-
lines.push(`# §${args.id} — ${fm["title"]}`);
|
|
476
|
-
lines.push("");
|
|
477
|
-
lines.push("## Constraint");
|
|
478
|
-
lines.push("");
|
|
479
|
-
lines.push(args.classification.suggestedInvariant.trim());
|
|
480
|
-
lines.push("");
|
|
481
|
-
lines.push("## Source comment");
|
|
482
|
-
lines.push("");
|
|
483
|
-
lines.push("```");
|
|
484
|
-
lines.push(args.block.raw);
|
|
485
|
-
lines.push("```");
|
|
486
|
-
lines.push("");
|
|
487
|
-
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
488
|
-
return { absPath: abs, relPath: rel };
|
|
527
|
+
log.info({
|
|
528
|
+
files: allFiles.size,
|
|
529
|
+
decs: Array.from(decsByFile.values()).reduce((acc, s) => acc + s.size, 0),
|
|
530
|
+
invs: Array.from(invsByFile.values()).reduce((acc, s) => acc + s.size, 0),
|
|
531
|
+
}, "scope-index updated with cite tokens from strip-replace");
|
|
489
532
|
}
|
|
490
533
|
function writeYaml(path, payload) {
|
|
491
534
|
mkdirSync(dirname(path), { recursive: true });
|