@isaacriehm/cairn-core 0.4.3 → 0.6.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 +162 -0
- package/dist/align-undo/log.js.map +1 -0
- package/dist/align-undo/undo.d.ts +65 -0
- package/dist/align-undo/undo.js +397 -0
- package/dist/align-undo/undo.js.map +1 -0
- package/dist/attention/bulk-accept.js +9 -22
- package/dist/attention/bulk-accept.js.map +1 -1
- package/dist/attention/dedup.js +1 -47
- package/dist/attention/dedup.js.map +1 -1
- package/dist/attention/serve/api.js +3 -17
- package/dist/attention/serve/api.js.map +1 -1
- package/dist/attention/serve/index.js +3 -3
- package/dist/attention/serve/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 +7 -0
- package/dist/fix-align/index.js +6 -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/fix-align/sentinel.d.ts +59 -0
- package/dist/fix-align/sentinel.js +149 -0
- package/dist/fix-align/sentinel.js.map +1 -0
- package/dist/fs.d.ts +5 -0
- package/dist/fs.js +11 -0
- package/dist/fs.js.map +1 -0
- package/dist/gc/apply.js +4 -4
- package/dist/gc/apply.js.map +1 -1
- 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 +56 -0
- package/dist/ground/anchor-map.js.map +1 -0
- package/dist/ground/frontmatter.d.ts +12 -0
- package/dist/ground/frontmatter.js +28 -0
- package/dist/ground/frontmatter.js.map +1 -1
- package/dist/ground/index.d.ts +10 -3
- package/dist/ground/index.js +9 -3
- 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 +135 -10
- package/dist/ground/schemas.js.map +1 -1
- package/dist/ground/scope-index.js +4 -4
- 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 +79 -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 +62 -0
- package/dist/ground/sot-cache.js.map +1 -0
- package/dist/ground/topic-index.d.ts +27 -0
- package/dist/ground/topic-index.js +82 -0
- package/dist/ground/topic-index.js.map +1 -0
- 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/sot-align.d.ts +166 -0
- package/dist/hooks/post-tool-use/sot-align.js +1306 -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 -410
- 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.js +6 -6
- package/dist/init/mapper.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 -162
- 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 +214 -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 -361
- 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/schemas.d.ts +45 -8
- package/dist/mcp/schemas.js +43 -7
- 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/index.d.ts +1 -1
- package/dist/mcp/tools/index.js +3 -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 +5 -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 +781 -5
- 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
|
@@ -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,141 +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
|
-
rationale: block.prose,
|
|
87
|
-
capture_source: "init-source-comments",
|
|
88
|
-
source_file: block.file,
|
|
89
|
-
source_offset: block.startLine,
|
|
90
|
-
raw: block.raw,
|
|
91
|
-
}, existingIds);
|
|
92
|
-
existingIds.add(id);
|
|
93
|
-
const confidence = args.globs !== undefined
|
|
94
|
-
? scoreDecDraft({
|
|
95
|
-
sourceFile: block.file,
|
|
96
|
-
prose: block.prose,
|
|
97
|
-
title: cls.suggestedDecDraft,
|
|
98
|
-
rawComment: block.raw,
|
|
99
|
-
globs: args.globs,
|
|
100
|
-
...(args.pilotModule !== undefined ? { pilotModule: args.pilotModule } : {}),
|
|
101
|
-
})
|
|
102
|
-
: undefined;
|
|
103
|
-
if (args.dryRun !== true) {
|
|
104
|
-
const written = writeDecDraft({
|
|
105
|
-
repoRoot,
|
|
106
|
-
id,
|
|
107
|
-
block,
|
|
108
|
-
classification: cls,
|
|
109
|
-
generatedAt: nowIso,
|
|
110
|
-
...(confidence !== undefined ? { confidence } : {}),
|
|
111
|
-
});
|
|
112
|
-
decDraftsWritten.push({
|
|
113
|
-
id,
|
|
114
|
-
path: written.relPath,
|
|
115
|
-
sourceFile: block.file,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
decDraftsWritten.push({
|
|
120
|
-
id,
|
|
121
|
-
path: `.cairn/ground/decisions/_inbox/${id}.draft.md`,
|
|
122
|
-
sourceFile: block.file,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
104
|
+
if (cls.kind !== "rationale" && cls.kind !== "constraint") {
|
|
105
|
+
skipped.push({ blockId: block.id, reason: `kind=${cls.kind}` });
|
|
106
|
+
continue;
|
|
125
107
|
}
|
|
126
|
-
if (cls.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
start_line: block.startLine,
|
|
131
|
-
end_line: block.endLine,
|
|
132
|
-
proposed: cls.suggestedInvariant,
|
|
133
|
-
canonical_topic: cls.suggestedCanonicalTopic,
|
|
108
|
+
if (cls.failed) {
|
|
109
|
+
skipped.push({
|
|
110
|
+
blockId: block.id,
|
|
111
|
+
reason: `classifier failed: ${cls.errorMessage ?? "unknown"}`,
|
|
134
112
|
});
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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({
|
|
179
238
|
blockId: block.id,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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,
|
|
185
249
|
});
|
|
186
250
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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,
|
|
195
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
|
+
}
|
|
196
328
|
}
|
|
197
329
|
}
|
|
330
|
+
// ── 8. Audit yaml ───────────────────────────────────────────────
|
|
198
331
|
const auditRelPath = `.cairn/baseline/source-comments-${tsSlug}.yaml`;
|
|
199
332
|
const auditPath = join(repoRoot, auditRelPath);
|
|
200
|
-
let invariantProposalsPath = null;
|
|
201
|
-
let canonicalCitationsPath = null;
|
|
202
333
|
if (args.dryRun !== true) {
|
|
203
334
|
writeYaml(auditPath, {
|
|
204
335
|
run_at: nowIso,
|
|
@@ -213,6 +344,9 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
213
344
|
batches_failed: classifyResult.batchesFailed,
|
|
214
345
|
input_tokens: classifyResult.inputTokens,
|
|
215
346
|
output_tokens: classifyResult.outputTokens,
|
|
347
|
+
decs_written: decsWritten.length,
|
|
348
|
+
invs_written: invsWritten.length,
|
|
349
|
+
cites_emitted: citesEmitted.length,
|
|
216
350
|
blocks: walk.blocks.map((b, idx) => ({
|
|
217
351
|
block_id: b.id,
|
|
218
352
|
file: b.file,
|
|
@@ -227,123 +361,36 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
227
361
|
end_offset: b.endOffset,
|
|
228
362
|
raw: b.raw,
|
|
229
363
|
classification: classifyResult.classifications[idx] ?? null,
|
|
364
|
+
resolution: serializeResolution(resolutionByBlockId.get(b.id)),
|
|
230
365
|
})),
|
|
231
366
|
});
|
|
232
|
-
if (invariantProposals.length > 0) {
|
|
233
|
-
const rel = `.cairn/baseline/invariant-proposals-${tsSlug}.yaml`;
|
|
234
|
-
invariantProposalsPath = join(repoRoot, rel);
|
|
235
|
-
writeYaml(invariantProposalsPath, {
|
|
236
|
-
run_at: nowIso,
|
|
237
|
-
proposals: invariantProposals,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (canonicalCitations.length > 0) {
|
|
241
|
-
const rel = `.cairn/baseline/canonical-citations-${tsSlug}.yaml`;
|
|
242
|
-
canonicalCitationsPath = join(repoRoot, rel);
|
|
243
|
-
writeYaml(canonicalCitationsPath, {
|
|
244
|
-
run_at: nowIso,
|
|
245
|
-
citations: canonicalCitations,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
367
|
}
|
|
249
368
|
log.info({
|
|
250
369
|
files: walk.files.length,
|
|
251
370
|
blocks: walk.blocks.length,
|
|
252
371
|
kindCounts,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
372
|
+
decs: decsWritten.length,
|
|
373
|
+
invs: invsWritten.length,
|
|
374
|
+
cites: citesEmitted.length,
|
|
375
|
+
stripApplied: stripItemsApplied,
|
|
376
|
+
stripSkipped: stripItemsSkipped,
|
|
256
377
|
inputTokens: classifyResult.inputTokens,
|
|
257
378
|
outputTokens: classifyResult.outputTokens,
|
|
258
379
|
}, "source-comments ingestion complete");
|
|
259
|
-
// Rebuild the invariants ledger after writing new INV-<NNNN>.md files so
|
|
260
|
-
// that §INV-NNNN tokens are resolvable before the strip-replace stage writes
|
|
261
|
-
// bare citations into source. Lens + MCP read tools resolve through the
|
|
262
|
-
// ledger, not by re-walking the dir.
|
|
263
|
-
if (args.dryRun !== true && invariantsWritten.length > 0) {
|
|
264
|
-
try {
|
|
265
|
-
writeInvariantsLedger({ repoRoot });
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
log.warn({ err: err instanceof Error ? err.message : String(err) }, "invariants ledger rebuild failed");
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Strip the original constraint comment from each source file and
|
|
272
|
-
// replace with a bare `// §INV-NNNN` cite. Adoption assumes a clean
|
|
273
|
-
// working tree (init phase 1's preflight); pass `overwrite` for
|
|
274
|
-
// every file so dirty-skip doesn't bite us — the operator
|
|
275
|
-
// consented to source mutation when they consented to adoption.
|
|
276
|
-
let invariantStripFilesModified = 0;
|
|
277
|
-
let invariantStripItemsApplied = 0;
|
|
278
|
-
let invariantStripItemsSkipped = 0;
|
|
279
|
-
let invariantStripOutcomes = [];
|
|
280
|
-
let invariantStripError = null;
|
|
281
|
-
if (args.dryRun !== true && invariantStripItems.length > 0) {
|
|
282
|
-
log.info({
|
|
283
|
-
items: invariantStripItems.length,
|
|
284
|
-
files: [...new Set(invariantStripItems.map((it) => it.file))],
|
|
285
|
-
}, "invariant strip-replace: starting");
|
|
286
|
-
try {
|
|
287
|
-
const dirtyDecisions = {};
|
|
288
|
-
for (const item of invariantStripItems)
|
|
289
|
-
dirtyDecisions[item.file] = "overwrite";
|
|
290
|
-
const result = applyStripReplace({
|
|
291
|
-
repoRoot,
|
|
292
|
-
items: invariantStripItems,
|
|
293
|
-
dirtyDecisions,
|
|
294
|
-
});
|
|
295
|
-
invariantStripFilesModified = result.filesModified;
|
|
296
|
-
invariantStripItemsApplied = result.itemsApplied;
|
|
297
|
-
invariantStripItemsSkipped = result.itemsSkipped;
|
|
298
|
-
invariantStripOutcomes = result.files.map((o) => ({
|
|
299
|
-
file: o.file,
|
|
300
|
-
applied: o.itemsApplied,
|
|
301
|
-
skipped: o.itemsSkipped.map((s) => ({ blockId: s.blockId, reason: s.reason })),
|
|
302
|
-
fileSkipReason: o.fileSkipReason ?? null,
|
|
303
|
-
}));
|
|
304
|
-
log.info({
|
|
305
|
-
filesModified: result.filesModified,
|
|
306
|
-
itemsApplied: result.itemsApplied,
|
|
307
|
-
itemsSkipped: result.itemsSkipped,
|
|
308
|
-
outcomes: invariantStripOutcomes,
|
|
309
|
-
}, "invariant strip-replace: complete");
|
|
310
|
-
// Now that we know which §INV-NNNN landed in which file, populate
|
|
311
|
-
// the scope-index for those files. Phase 3 mapper ran before any
|
|
312
|
-
// invariants existed, so its scope_index entries for these files
|
|
313
|
-
// had empty `invariants: []` arrays. Without this update the
|
|
314
|
-
// read-enricher legend's "Invariants in scope" header stays blank
|
|
315
|
-
// even though the source carries the cite tokens.
|
|
316
|
-
try {
|
|
317
|
-
updateScopeIndexFromStripItems(repoRoot, invariantStripItems);
|
|
318
|
-
}
|
|
319
|
-
catch (err) {
|
|
320
|
-
log.warn({ err: err instanceof Error ? err.message : String(err) }, "scope-index update from strip items failed");
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
catch (err) {
|
|
324
|
-
invariantStripError = err instanceof Error ? err.message : String(err);
|
|
325
|
-
log.warn({ err: invariantStripError }, "invariant strip-replace failed");
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else if (invariantStripItems.length === 0) {
|
|
329
|
-
log.info("invariant strip-replace: no items (no constraint blocks classified)");
|
|
330
|
-
}
|
|
331
380
|
return {
|
|
332
381
|
walk,
|
|
333
382
|
classifications: classifyResult.classifications,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
383
|
+
decsWritten,
|
|
384
|
+
invsWritten,
|
|
385
|
+
citesEmitted,
|
|
386
|
+
skipped,
|
|
387
|
+
stripFilesModified,
|
|
388
|
+
stripItemsApplied,
|
|
389
|
+
stripItemsSkipped,
|
|
390
|
+
stripOutcomes,
|
|
391
|
+
stripError,
|
|
343
392
|
auditPath,
|
|
344
393
|
auditRelPath,
|
|
345
|
-
invariantProposalsPath,
|
|
346
|
-
canonicalCitationsPath,
|
|
347
394
|
inputTokens: classifyResult.inputTokens,
|
|
348
395
|
outputTokens: classifyResult.outputTokens,
|
|
349
396
|
batchesRun: classifyResult.batchesRun,
|
|
@@ -352,56 +399,121 @@ export async function runSourceCommentsIngestion(args) {
|
|
|
352
399
|
};
|
|
353
400
|
}
|
|
354
401
|
/* -------------------------------------------------------------------------- */
|
|
355
|
-
/*
|
|
402
|
+
/* Helpers */
|
|
356
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
|
+
}
|
|
357
456
|
/**
|
|
358
|
-
* After the strip-replace pass inserts
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* Each file's existing invariants array gets unioned with the new IDs,
|
|
366
|
-
* de-duplicated, and re-coerced (defense-in-depth). Files absent from
|
|
367
|
-
* the scope-index get a fresh entry. Decisions arrays are left
|
|
368
|
-
* untouched — DEC strips happen at accept-time via cairn-attention,
|
|
369
|
-
* 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.
|
|
370
463
|
*/
|
|
371
464
|
function updateScopeIndexFromStripItems(repoRoot, items) {
|
|
372
465
|
if (items.length === 0)
|
|
373
466
|
return;
|
|
374
|
-
const
|
|
375
|
-
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,})/;
|
|
376
471
|
for (const item of items) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
}
|
|
387
495
|
}
|
|
388
|
-
set.add(id);
|
|
389
496
|
}
|
|
390
|
-
if (
|
|
497
|
+
if (decsByFile.size === 0 && invsByFile.size === 0)
|
|
391
498
|
return;
|
|
392
499
|
const existing = readScopeIndex(repoRoot) ?? {
|
|
393
500
|
generated: new Date().toISOString(),
|
|
394
501
|
files: {},
|
|
395
502
|
};
|
|
396
|
-
|
|
503
|
+
const allFiles = new Set([...decsByFile.keys(), ...invsByFile.keys()]);
|
|
504
|
+
for (const file of allFiles) {
|
|
397
505
|
const prior = existing.files[file];
|
|
398
|
-
const
|
|
506
|
+
const mergedDecs = coerceDecisionIds([
|
|
507
|
+
...(prior?.decisions ?? []),
|
|
508
|
+
...(decsByFile.get(file) ?? []),
|
|
509
|
+
]);
|
|
510
|
+
const mergedInvs = coerceInvariantIds([
|
|
399
511
|
...(prior?.invariants ?? []),
|
|
400
|
-
...
|
|
512
|
+
...(invsByFile.get(file) ?? []),
|
|
401
513
|
]);
|
|
402
514
|
const next = {
|
|
403
|
-
decisions:
|
|
404
|
-
invariants:
|
|
515
|
+
decisions: mergedDecs,
|
|
516
|
+
invariants: mergedInvs,
|
|
405
517
|
};
|
|
406
518
|
if (prior?.unscoped === true)
|
|
407
519
|
next.unscoped = true;
|
|
@@ -412,92 +524,11 @@ function updateScopeIndexFromStripItems(repoRoot, items) {
|
|
|
412
524
|
files: existing.files,
|
|
413
525
|
};
|
|
414
526
|
writeScopeIndex(repoRoot, updated);
|
|
415
|
-
log.info({
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
mkdirSync(inboxDir, { recursive: true });
|
|
421
|
-
const filename = `${args.id}.draft.md`;
|
|
422
|
-
const abs = join(inboxDir, filename);
|
|
423
|
-
const rel = `.cairn/ground/decisions/_inbox/${filename}`;
|
|
424
|
-
const fm = {
|
|
425
|
-
id: args.id,
|
|
426
|
-
title: args.classification.suggestedDecDraft || `(untitled — from ${args.block.file})`,
|
|
427
|
-
type: "adr",
|
|
428
|
-
status: "draft-from-source-comment",
|
|
429
|
-
audience: "dual",
|
|
430
|
-
generated: args.generatedAt,
|
|
431
|
-
"verified-at": args.generatedAt,
|
|
432
|
-
decided_at: args.generatedAt,
|
|
433
|
-
decided_by: "cairn-init",
|
|
434
|
-
capture_source: "init-source-comments",
|
|
435
|
-
capture_confidence: args.confidence ?? "medium",
|
|
436
|
-
sourceFile: args.block.file,
|
|
437
|
-
sourceRange: `${args.block.startLine}-${args.block.endLine}`,
|
|
438
|
-
blockId: args.block.id,
|
|
439
|
-
canonicalTopic: args.classification.suggestedCanonicalTopic,
|
|
440
|
-
};
|
|
441
|
-
const lines = [];
|
|
442
|
-
lines.push("---");
|
|
443
|
-
lines.push(stringifyYaml(fm).trimEnd());
|
|
444
|
-
lines.push("---");
|
|
445
|
-
lines.push("");
|
|
446
|
-
lines.push(`# ${args.id} — ${fm["title"]}`);
|
|
447
|
-
lines.push("");
|
|
448
|
-
lines.push("## Source comment");
|
|
449
|
-
lines.push("");
|
|
450
|
-
lines.push("```");
|
|
451
|
-
lines.push(args.block.raw);
|
|
452
|
-
lines.push("```");
|
|
453
|
-
lines.push("");
|
|
454
|
-
lines.push("## Proposed rationale");
|
|
455
|
-
lines.push("");
|
|
456
|
-
lines.push(args.block.prose);
|
|
457
|
-
lines.push("");
|
|
458
|
-
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
459
|
-
return { absPath: abs, relPath: rel };
|
|
460
|
-
}
|
|
461
|
-
function writeInvariantFile(args) {
|
|
462
|
-
const dir = invariantsDir(args.repoRoot);
|
|
463
|
-
mkdirSync(dir, { recursive: true });
|
|
464
|
-
const filename = `${args.id}.md`;
|
|
465
|
-
const abs = join(dir, filename);
|
|
466
|
-
const rel = `.cairn/ground/invariants/${filename}`;
|
|
467
|
-
const fm = {
|
|
468
|
-
id: args.id,
|
|
469
|
-
title: args.classification.suggestedInvariant.split("\n")[0]?.slice(0, 120) ?? args.id,
|
|
470
|
-
type: "invariant",
|
|
471
|
-
status: "active",
|
|
472
|
-
audience: "dual",
|
|
473
|
-
generated: args.generatedAt,
|
|
474
|
-
"verified-at": args.generatedAt,
|
|
475
|
-
capture_confidence: args.confidence ?? "medium",
|
|
476
|
-
source_decision: null,
|
|
477
|
-
sourceFile: args.block.file,
|
|
478
|
-
sourceRange: `${args.block.startLine}-${args.block.endLine}`,
|
|
479
|
-
blockId: args.block.id,
|
|
480
|
-
canonicalTopic: args.classification.suggestedCanonicalTopic,
|
|
481
|
-
};
|
|
482
|
-
const lines = [];
|
|
483
|
-
lines.push("---");
|
|
484
|
-
lines.push(stringifyYaml(fm).trimEnd());
|
|
485
|
-
lines.push("---");
|
|
486
|
-
lines.push("");
|
|
487
|
-
lines.push(`# §${args.id} — ${fm["title"]}`);
|
|
488
|
-
lines.push("");
|
|
489
|
-
lines.push("## Constraint");
|
|
490
|
-
lines.push("");
|
|
491
|
-
lines.push(args.classification.suggestedInvariant.trim());
|
|
492
|
-
lines.push("");
|
|
493
|
-
lines.push("## Source comment");
|
|
494
|
-
lines.push("");
|
|
495
|
-
lines.push("```");
|
|
496
|
-
lines.push(args.block.raw);
|
|
497
|
-
lines.push("```");
|
|
498
|
-
lines.push("");
|
|
499
|
-
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
500
|
-
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");
|
|
501
532
|
}
|
|
502
533
|
function writeYaml(path, payload) {
|
|
503
534
|
mkdirSync(dirname(path), { recursive: true });
|