@isaacriehm/cairn-core 0.4.3 → 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/dedup.js +1 -47
- package/dist/attention/dedup.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 +126 -1
- package/dist/ground/schemas.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/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 +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 -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/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 +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 -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.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/resolve-attention.d.ts +2 -2
- package/dist/mcp/tools/resolve-attention.js +828 -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,36 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phase 7c orchestrator
|
|
2
|
+
* Phase 7c orchestrator (v0.5.0 SoT model).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Plan §5.4 algorithm:
|
|
5
|
+
* 1. Discover sections in `CLAUDE.md`, `AGENTS.md`, `.claude/rules/*.md`.
|
|
6
|
+
* 2. Topic-index lookup (built by phase 5b) before classification:
|
|
7
|
+
* - **Match** — slug already owns a docs/CLAUDE.md/AGENTS.md/rule
|
|
8
|
+
* SoT and was emitted by an earlier phase. Phase 7c records the
|
|
9
|
+
* cite (no source rewrite — operator's narrative stays intact)
|
|
10
|
+
* and skips emit.
|
|
11
|
+
* - **Net-new** — slug is in topic-index but not yet emitted.
|
|
12
|
+
* Phase 7c classifies the section via Haiku (kind only:
|
|
13
|
+
* decision / domain-rule / constraint / informational), emits
|
|
14
|
+
* a verbatim DEC/INV via `sot-emit` with `sot_kind: "path"` +
|
|
15
|
+
* `sot_path: <file>#<anchor>`, auto-promotes (`status: accepted`).
|
|
16
|
+
* 3. Conflict detection — for each freshly emitted entity, scan
|
|
17
|
+
* accepted DECs/INVs in `sot-cache.yaml` for high Jaccard overlap
|
|
18
|
+
* against the new body, then run a Haiku contradiction judge per
|
|
19
|
+
* candidate (`contradict | agree | unrelated`). On `contradict`,
|
|
20
|
+
* write `.cairn/ground/conflicts/<new>__<other>.md` with both
|
|
21
|
+
* prose sides + Haiku reasoning. The cairn-attention skill renders
|
|
22
|
+
* these per §5.4.1; **no source rewrite ever fires from conflicts**.
|
|
23
|
+
* 4. Auto-promote — every novel entity ships `status: accepted`. The
|
|
24
|
+
* `_inbox/` draft queue is gone (the v0.4.x review surface was the
|
|
25
|
+
* v0.5.0 pivot's primary motivation).
|
|
6
26
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Resilient: a single Haiku failure marks the section "informational" and
|
|
16
|
-
* continues. All output paths captured in the result so the skill can surface
|
|
17
|
-
* them.
|
|
27
|
+
* Output side-effects (all relative to repoRoot):
|
|
28
|
+
* - `.cairn/ground/decisions/<DEC-id>.md` (one per novel decision/domain-rule)
|
|
29
|
+
* - `.cairn/ground/invariants/<INV-id>.md` (one per novel constraint)
|
|
30
|
+
* - `.cairn/ground/topic-index.yaml` (extended w/ dec_id stamps)
|
|
31
|
+
* - `.cairn/ground/sot-bindings.yaml` (forward+reverse for new ids)
|
|
32
|
+
* - `.cairn/ground/sot-cache.yaml` (token cache for Layer A)
|
|
33
|
+
* - `.cairn/ground/conflicts/<a>__<b>.md` (one per contradiction)
|
|
34
|
+
* - `.cairn/baseline/rules-merge-<ISO>.yaml` (full audit)
|
|
18
35
|
*/
|
|
19
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
37
|
import { dirname, join } from "node:path";
|
|
21
38
|
import { stringify as stringifyYaml } from "yaml";
|
|
22
39
|
import { runClaude } from "../../claude/index.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
40
|
+
import { conflictsDir, emptyAnchorMap, emptySotBindings, emptySotCache, emptyTopicIndex, readAnchorMap, readSotBindings, readSotCache, readTopicIndex, setSotCacheEntry, topicSlug, writeAnchorMap, writeSotBindings, writeSotCache, writeTopicIndex, } from "../../ground/index.js";
|
|
41
|
+
import { writeDecisionsLedger, writeInvariantsLedger } from "../../ground/ledgers.js";
|
|
25
42
|
import { logger } from "../../logger.js";
|
|
43
|
+
import { jaccard, tokenize } from "../../text/jaccard.js";
|
|
44
|
+
import { emitFromTopicIndex } from "../sot-emit.js";
|
|
26
45
|
import { discoverRuleSources } from "./discover.js";
|
|
27
46
|
import { parseRuleSections } from "./parse-sections.js";
|
|
28
47
|
const log = logger("init.rules-merge.ingest");
|
|
29
48
|
const PER_SECTION_TIMEOUT_MS = 60_000;
|
|
30
49
|
const SECTION_BODY_CAP = 4_000;
|
|
31
50
|
const CONCURRENCY = 4;
|
|
51
|
+
const CAPTURE_SOURCE = "init-rules-merge";
|
|
52
|
+
/** Conflict-scan tuning. */
|
|
53
|
+
const CONFLICT_JACCARD_THRESHOLD = 0.4;
|
|
54
|
+
const CONFLICT_MAX_CANDIDATES_PER_EMIT = 3;
|
|
55
|
+
const CONFLICT_MAX_JUDGE_CALLS = 25;
|
|
56
|
+
const CONFLICT_BODY_CAP = 1_500;
|
|
57
|
+
const PER_CONTRADICTION_TIMEOUT_MS = 30_000;
|
|
32
58
|
/* -------------------------------------------------------------------------- */
|
|
33
|
-
/*
|
|
59
|
+
/* Schemas + prompts */
|
|
34
60
|
/* -------------------------------------------------------------------------- */
|
|
35
61
|
const CLASSIFY_SCHEMA = {
|
|
36
62
|
type: "object",
|
|
@@ -39,29 +65,40 @@ const CLASSIFY_SCHEMA = {
|
|
|
39
65
|
properties: {
|
|
40
66
|
kind: {
|
|
41
67
|
type: "string",
|
|
42
|
-
enum: ["
|
|
68
|
+
enum: ["decision", "domain-rule", "constraint", "informational"],
|
|
43
69
|
},
|
|
44
|
-
proposed_dec_title: { type: "string" },
|
|
45
|
-
proposed_rationale: { type: "string" },
|
|
46
|
-
conflicts_with: { type: "string" },
|
|
47
70
|
},
|
|
48
71
|
};
|
|
49
|
-
const CLASSIFY_SYSTEM = `You classify markdown sections of project-rule files for Cairn
|
|
72
|
+
const CLASSIFY_SYSTEM = `You classify markdown sections of project-rule files for Cairn's Single-Source-of-Truth ledger.
|
|
73
|
+
|
|
74
|
+
Each section comes from CLAUDE.md, AGENTS.md, or a .claude/rules/*.md file.
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
Return JSON matching the schema. \`kind\` choices:
|
|
77
|
+
- "decision" paragraph describes a binding decision or architectural choice
|
|
78
|
+
- "domain-rule" paragraph states a domain rule developers must follow (treated as a decision in the ledger)
|
|
79
|
+
- "constraint" paragraph states a hard constraint / invariant (must / must not / never / always)
|
|
80
|
+
- "informational" TOC, walkthrough, history, formatting notes — nothing actionable
|
|
81
|
+
|
|
82
|
+
Be conservative — false-positive ledger entries pollute ground state worse than missed capture.
|
|
83
|
+
Default to "informational" when uncertain.`;
|
|
84
|
+
const CONTRADICTION_SCHEMA = {
|
|
85
|
+
type: "object",
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
required: ["verdict"],
|
|
88
|
+
properties: {
|
|
89
|
+
verdict: { type: "string", enum: ["contradict", "agree", "unrelated"] },
|
|
90
|
+
reasoning: { type: "string" },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const CONTRADICTION_SYSTEM = `You compare two project-rule statements for contradiction.
|
|
52
94
|
|
|
53
|
-
Return JSON
|
|
54
|
-
- "rule-net-new" the section states a binding rule cairn doesn't yet have
|
|
55
|
-
- "rule-conflict" the section contradicts an existing cairn rule (provide conflicts_with id when known)
|
|
56
|
-
- "informational" TOC, walkthrough, history, formatting notes — nothing to ingest
|
|
57
|
-
- "operator-keep" the section is wrapped in keep-markers (rare — caller usually filters first)
|
|
95
|
+
Return JSON: { "verdict": "contradict" | "agree" | "unrelated", "reasoning": "<one sentence>" }.
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
- conflicts_with DEC-NNNN or §INV-NNNN id (only when kind = "rule-conflict")
|
|
97
|
+
- "contradict" the two statements cannot both be true / followed at once
|
|
98
|
+
- "agree" the two statements describe the same rule with compatible wording
|
|
99
|
+
- "unrelated" the statements address different topics
|
|
63
100
|
|
|
64
|
-
Be conservative.
|
|
101
|
+
Be conservative on "contradict" — only flag a true logical contradiction (one says X, the other says NOT X). Surface-level differences in tone or scope are NOT contradictions.`;
|
|
65
102
|
/* -------------------------------------------------------------------------- */
|
|
66
103
|
/* Public */
|
|
67
104
|
/* -------------------------------------------------------------------------- */
|
|
@@ -69,7 +106,10 @@ export async function runRulesMerge(args) {
|
|
|
69
106
|
const repoRoot = args.repoRoot;
|
|
70
107
|
const nowIso = args.nowIso ?? new Date().toISOString();
|
|
71
108
|
const tsSlug = nowIso.replace(/[:.]/g, "-").slice(0, 19);
|
|
109
|
+
// ── 1. Discover + walk sections ──────────────────────────────────
|
|
72
110
|
const sources = discoverRuleSources(repoRoot);
|
|
111
|
+
const ruleFilesSet = new Set(sources.map((s) => s.path));
|
|
112
|
+
const sectionsBySlug = new Map();
|
|
73
113
|
const allClassifications = [];
|
|
74
114
|
const jobs = [];
|
|
75
115
|
for (const source of sources) {
|
|
@@ -84,27 +124,43 @@ export async function runRulesMerge(args) {
|
|
|
84
124
|
const sections = parseRuleSections(body);
|
|
85
125
|
for (const section of sections) {
|
|
86
126
|
if (section.level === 0)
|
|
87
|
-
continue;
|
|
127
|
+
continue;
|
|
88
128
|
if (section.protectedByKeepMarker) {
|
|
89
129
|
allClassifications.push({
|
|
90
130
|
source: source.path,
|
|
91
131
|
level: section.level,
|
|
92
132
|
title: section.title,
|
|
93
133
|
startOffset: section.startOffset,
|
|
134
|
+
slug: "",
|
|
94
135
|
kind: "operator-keep",
|
|
95
|
-
proposedDecTitle: "",
|
|
96
|
-
proposedRationale: "",
|
|
97
|
-
conflictsWith: "",
|
|
98
136
|
failed: false,
|
|
99
137
|
});
|
|
100
138
|
continue;
|
|
101
139
|
}
|
|
102
|
-
|
|
140
|
+
const bodyMinusHeading = stripLeadingHeading(section.body);
|
|
141
|
+
if (bodyMinusHeading.length === 0) {
|
|
142
|
+
// Empty body after heading strip — nothing to classify or fingerprint.
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const slug = topicSlug(bodyMinusHeading);
|
|
146
|
+
const anchor = headingToAnchor(section.title);
|
|
147
|
+
const job = { source, section, slug, bodyMinusHeading, anchor };
|
|
148
|
+
jobs.push(job);
|
|
149
|
+
sectionsBySlug.set(slug, {
|
|
150
|
+
sourcePath: source.path,
|
|
151
|
+
sectionTitle: section.title,
|
|
152
|
+
anchor,
|
|
153
|
+
bodyMinusHeading,
|
|
154
|
+
});
|
|
103
155
|
}
|
|
104
156
|
}
|
|
157
|
+
// ── 2. Classify each non-keep section ────────────────────────────
|
|
105
158
|
if (args.mockClassify !== undefined) {
|
|
106
159
|
for (const [idx, job] of jobs.entries()) {
|
|
107
|
-
|
|
160
|
+
const cls = args.mockClassify(job.section, job.source);
|
|
161
|
+
// Mock callers may not stamp `slug`; fill it in for them so the
|
|
162
|
+
// emit filter has something to look up.
|
|
163
|
+
allClassifications.push({ ...cls, slug: cls.slug.length > 0 ? cls.slug : job.slug });
|
|
108
164
|
args.onSectionProgress?.({ index: idx + 1, total: jobs.length });
|
|
109
165
|
}
|
|
110
166
|
}
|
|
@@ -118,7 +174,7 @@ export async function runRulesMerge(args) {
|
|
|
118
174
|
const job = jobs[idx];
|
|
119
175
|
if (job === undefined)
|
|
120
176
|
continue;
|
|
121
|
-
const cls = await classifySection(job
|
|
177
|
+
const cls = await classifySection(job);
|
|
122
178
|
allClassifications.push(cls);
|
|
123
179
|
completed += 1;
|
|
124
180
|
args.onSectionProgress?.({ index: completed, total });
|
|
@@ -127,53 +183,156 @@ export async function runRulesMerge(args) {
|
|
|
127
183
|
const workers = Array.from({ length: Math.min(CONCURRENCY, jobs.length) }, () => worker());
|
|
128
184
|
await Promise.all(workers);
|
|
129
185
|
}
|
|
130
|
-
|
|
131
|
-
const decDraftsWritten = [];
|
|
132
|
-
const conflictRows = [];
|
|
133
|
-
const existingIds = args.existingDecIds ?? scanExistingDecisionIds(repoRoot);
|
|
186
|
+
const kindBySlug = new Map();
|
|
134
187
|
for (const cls of allClassifications) {
|
|
135
|
-
if (cls.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
188
|
+
if (cls.slug.length > 0)
|
|
189
|
+
kindBySlug.set(cls.slug, cls.kind);
|
|
190
|
+
}
|
|
191
|
+
// ── 3. Read ground state + identify cite-existing slugs ──────────
|
|
192
|
+
let topicIndex = readTopicIndex(repoRoot);
|
|
193
|
+
if (Object.keys(topicIndex.topics).length === 0)
|
|
194
|
+
topicIndex = emptyTopicIndex();
|
|
195
|
+
let anchorMap = readAnchorMap(repoRoot);
|
|
196
|
+
if (Object.keys(anchorMap.anchors).length === 0)
|
|
197
|
+
anchorMap = emptyAnchorMap();
|
|
198
|
+
const citesEmitted = [];
|
|
199
|
+
for (const [slug, ctx] of sectionsBySlug) {
|
|
200
|
+
const entry = topicIndex.topics[slug];
|
|
201
|
+
if (entry !== undefined && entry.dec_id !== undefined && !ruleFilesSet.has(entry.sot_source)) {
|
|
202
|
+
// Slug already SoT'd by phase 6 (docs); operator's CLAUDE.md / AGENTS.md
|
|
203
|
+
// section is a cite of the same fact. No source rewrite — operator's
|
|
204
|
+
// narrative stays intact. Plan §5.4.1.
|
|
205
|
+
citesEmitted.push({ id: entry.dec_id, sourceFile: ctx.sourcePath, slug });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ── 4. Emit (sot-emit, sot_kind=path) ────────────────────────────
|
|
209
|
+
const emit = await emitFromTopicIndex({
|
|
210
|
+
repoRoot,
|
|
211
|
+
topicIndex,
|
|
212
|
+
anchorMap,
|
|
213
|
+
filter: (entry) => entry.dec_id === undefined &&
|
|
214
|
+
ruleFilesSet.has(entry.sot_source) &&
|
|
215
|
+
kindBySlug.has(entry.slug) &&
|
|
216
|
+
isEmittableKind(kindBySlug.get(entry.slug)),
|
|
217
|
+
classifier: async ({ entry }) => {
|
|
218
|
+
const ctx = sectionsBySlug.get(entry.slug);
|
|
219
|
+
const k = kindBySlug.get(entry.slug);
|
|
220
|
+
if (k === undefined || ctx === undefined)
|
|
221
|
+
return { kind: "skip", title: "" };
|
|
222
|
+
if (k === "constraint")
|
|
223
|
+
return { kind: "constraint", title: ctx.sectionTitle };
|
|
224
|
+
if (k === "decision" || k === "domain-rule") {
|
|
225
|
+
return { kind: "decision", title: ctx.sectionTitle };
|
|
153
226
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
227
|
+
return { kind: "skip", title: "" };
|
|
228
|
+
},
|
|
229
|
+
sot_kind: "path",
|
|
230
|
+
capture_source: CAPTURE_SOURCE,
|
|
231
|
+
});
|
|
232
|
+
topicIndex = emit.topicIndex;
|
|
233
|
+
if (args.dryRun !== true) {
|
|
234
|
+
persistGroundState({
|
|
235
|
+
repoRoot,
|
|
236
|
+
topicIndex,
|
|
237
|
+
anchorMap,
|
|
238
|
+
bindings: emit.bindings,
|
|
239
|
+
cache: emit.cache,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// ── 5. Build emitted records ─────────────────────────────────────
|
|
243
|
+
const decsWritten = [];
|
|
244
|
+
const invsWritten = [];
|
|
245
|
+
for (const rec of emit.emitted) {
|
|
246
|
+
const ctx = sectionsBySlug.get(rec.slug);
|
|
247
|
+
const target = {
|
|
248
|
+
id: rec.id,
|
|
249
|
+
path: rec.kind === "DEC"
|
|
250
|
+
? `.cairn/ground/decisions/${rec.id}.md`
|
|
251
|
+
: `.cairn/ground/invariants/${rec.id}.md`,
|
|
252
|
+
sourceFile: ctx?.sourcePath ?? rec.source_file,
|
|
253
|
+
slug: rec.slug,
|
|
254
|
+
status: "accepted",
|
|
255
|
+
};
|
|
256
|
+
if (rec.kind === "DEC")
|
|
257
|
+
decsWritten.push(target);
|
|
258
|
+
else
|
|
259
|
+
invsWritten.push(target);
|
|
260
|
+
}
|
|
261
|
+
// ── 6. Conflict scan ─────────────────────────────────────────────
|
|
262
|
+
const conflicts = [];
|
|
263
|
+
if (args.dryRun !== true && emit.emitted.length > 0) {
|
|
264
|
+
let judgeCalls = 0;
|
|
265
|
+
for (const rec of emit.emitted) {
|
|
266
|
+
if (judgeCalls >= CONFLICT_MAX_JUDGE_CALLS)
|
|
267
|
+
break;
|
|
268
|
+
const candidates = jaccardCandidates({
|
|
269
|
+
newId: rec.id,
|
|
270
|
+
newBody: rec.body,
|
|
271
|
+
cache: emit.cache,
|
|
272
|
+
threshold: CONFLICT_JACCARD_THRESHOLD,
|
|
273
|
+
topK: CONFLICT_MAX_CANDIDATES_PER_EMIT,
|
|
274
|
+
});
|
|
275
|
+
for (const cand of candidates) {
|
|
276
|
+
if (judgeCalls >= CONFLICT_MAX_JUDGE_CALLS)
|
|
277
|
+
break;
|
|
278
|
+
judgeCalls += 1;
|
|
279
|
+
const candBody = readEmittedBody(repoRoot, cand.id);
|
|
280
|
+
if (candBody === null)
|
|
281
|
+
continue;
|
|
282
|
+
const verdict = await runContradictionJudge({
|
|
283
|
+
newBody: rec.body,
|
|
284
|
+
candidateId: cand.id,
|
|
285
|
+
candidateBody: candBody,
|
|
286
|
+
mock: args.mockContradictionJudge,
|
|
159
287
|
});
|
|
288
|
+
if (verdict.verdict === "contradict") {
|
|
289
|
+
const conflictPath = writeConflictFile({
|
|
290
|
+
repoRoot,
|
|
291
|
+
newId: rec.id,
|
|
292
|
+
newBody: rec.body,
|
|
293
|
+
newSourceFile: rec.source_file,
|
|
294
|
+
otherId: cand.id,
|
|
295
|
+
otherBody: candBody,
|
|
296
|
+
otherSotPath: cand.sot_path,
|
|
297
|
+
reasoning: verdict.reasoning,
|
|
298
|
+
generatedAt: nowIso,
|
|
299
|
+
});
|
|
300
|
+
conflicts.push({
|
|
301
|
+
newId: rec.id,
|
|
302
|
+
otherId: cand.id,
|
|
303
|
+
conflictPath,
|
|
304
|
+
reasoning: verdict.reasoning,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
160
307
|
}
|
|
161
308
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
309
|
+
}
|
|
310
|
+
// ── 7. Ledger rebuilds ───────────────────────────────────────────
|
|
311
|
+
if (args.dryRun !== true) {
|
|
312
|
+
if (decsWritten.length > 0) {
|
|
313
|
+
try {
|
|
314
|
+
writeDecisionsLedger({ repoRoot });
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "decisions ledger rebuild failed");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (invsWritten.length > 0) {
|
|
321
|
+
try {
|
|
322
|
+
writeInvariantsLedger({ repoRoot });
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "invariants ledger rebuild failed");
|
|
326
|
+
}
|
|
169
327
|
}
|
|
170
328
|
}
|
|
329
|
+
// ── 8. Audit yaml ───────────────────────────────────────────────
|
|
171
330
|
const auditRelPath = `.cairn/baseline/rules-merge-${tsSlug}.yaml`;
|
|
172
331
|
const auditPath = join(repoRoot, auditRelPath);
|
|
173
|
-
let conflictsPath = null;
|
|
174
332
|
const kindCounts = {
|
|
175
|
-
|
|
176
|
-
"rule
|
|
333
|
+
decision: 0,
|
|
334
|
+
"domain-rule": 0,
|
|
335
|
+
constraint: 0,
|
|
177
336
|
informational: 0,
|
|
178
337
|
"operator-keep": 0,
|
|
179
338
|
};
|
|
@@ -186,58 +345,52 @@ export async function runRulesMerge(args) {
|
|
|
186
345
|
sources: sources.map((s) => ({ path: s.path, kind: s.kind, size: s.size })),
|
|
187
346
|
sections_total: allClassifications.length,
|
|
188
347
|
kind_counts: kindCounts,
|
|
348
|
+
decs_written: decsWritten.length,
|
|
349
|
+
invs_written: invsWritten.length,
|
|
350
|
+
cites_emitted: citesEmitted.length,
|
|
351
|
+
conflicts: conflicts.length,
|
|
189
352
|
classifications: allClassifications.map((c) => ({
|
|
190
353
|
source: c.source,
|
|
191
354
|
title: c.title,
|
|
192
355
|
level: c.level,
|
|
193
356
|
kind: c.kind,
|
|
357
|
+
slug: c.slug,
|
|
194
358
|
start_offset: c.startOffset,
|
|
195
|
-
proposed_dec_title: c.proposedDecTitle,
|
|
196
|
-
proposed_rationale: c.proposedRationale,
|
|
197
|
-
conflicts_with: c.conflictsWith,
|
|
198
359
|
failed: c.failed,
|
|
199
360
|
...(c.errorMessage !== undefined ? { error: c.errorMessage } : {}),
|
|
200
361
|
})),
|
|
201
362
|
});
|
|
202
|
-
if (conflictRows.length > 0) {
|
|
203
|
-
const rel = `.cairn/baseline/rule-conflicts-${tsSlug}.yaml`;
|
|
204
|
-
conflictsPath = join(repoRoot, rel);
|
|
205
|
-
writeYaml(conflictsPath, {
|
|
206
|
-
run_at: nowIso,
|
|
207
|
-
conflicts: conflictRows,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
363
|
}
|
|
211
364
|
log.info({
|
|
212
365
|
sources: sources.length,
|
|
213
366
|
sections: allClassifications.length,
|
|
214
367
|
kindCounts,
|
|
215
|
-
|
|
216
|
-
|
|
368
|
+
decs: decsWritten.length,
|
|
369
|
+
invs: invsWritten.length,
|
|
370
|
+
cites: citesEmitted.length,
|
|
371
|
+
conflicts: conflicts.length,
|
|
217
372
|
}, "rules merge complete");
|
|
218
373
|
return {
|
|
219
374
|
sources,
|
|
220
375
|
sectionsTotal: allClassifications.length,
|
|
221
376
|
classifications: allClassifications,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
377
|
+
decsWritten,
|
|
378
|
+
invsWritten,
|
|
379
|
+
citesEmitted,
|
|
380
|
+
conflicts,
|
|
225
381
|
auditPath,
|
|
226
382
|
auditRelPath,
|
|
227
383
|
kindCounts,
|
|
228
384
|
};
|
|
229
385
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const body = section.body.length > SECTION_BODY_CAP
|
|
235
|
-
? `${section.body.slice(0, SECTION_BODY_CAP)}\n…[truncated]`
|
|
236
|
-
: section.body;
|
|
386
|
+
async function classifySection(job) {
|
|
387
|
+
const body = job.bodyMinusHeading.length > SECTION_BODY_CAP
|
|
388
|
+
? `${job.bodyMinusHeading.slice(0, SECTION_BODY_CAP)}\n…[truncated]`
|
|
389
|
+
: job.bodyMinusHeading;
|
|
237
390
|
const prompt = [
|
|
238
|
-
`Source: ${source.path}`,
|
|
239
|
-
`Section title: ${section.title || "(preamble)"}`,
|
|
240
|
-
`Heading level: ${section.level}`,
|
|
391
|
+
`Source: ${job.source.path}`,
|
|
392
|
+
`Section title: ${job.section.title || "(preamble)"}`,
|
|
393
|
+
`Heading level: ${job.section.level}`,
|
|
241
394
|
"",
|
|
242
395
|
"Body:",
|
|
243
396
|
body,
|
|
@@ -253,94 +406,235 @@ async function classifySection(source, section) {
|
|
|
253
406
|
});
|
|
254
407
|
const parsed = result.parsed;
|
|
255
408
|
if (typeof parsed !== "object" || parsed === null) {
|
|
256
|
-
return informational(
|
|
257
|
-
source: source.path,
|
|
258
|
-
section,
|
|
259
|
-
failed: true,
|
|
260
|
-
errorMessage: "non-object response",
|
|
261
|
-
});
|
|
409
|
+
return informational(job, true, "non-object response");
|
|
262
410
|
}
|
|
263
411
|
const r = parsed;
|
|
264
412
|
const kindRaw = r["kind"];
|
|
265
|
-
const kind = kindRaw === "
|
|
266
|
-
kindRaw === "rule
|
|
267
|
-
kindRaw === "
|
|
413
|
+
const kind = kindRaw === "decision" ||
|
|
414
|
+
kindRaw === "domain-rule" ||
|
|
415
|
+
kindRaw === "constraint"
|
|
268
416
|
? kindRaw
|
|
269
417
|
: "informational";
|
|
270
418
|
return {
|
|
271
|
-
source: source.path,
|
|
272
|
-
level: section.level,
|
|
273
|
-
title: section.title,
|
|
274
|
-
startOffset: section.startOffset,
|
|
419
|
+
source: job.source.path,
|
|
420
|
+
level: job.section.level,
|
|
421
|
+
title: job.section.title,
|
|
422
|
+
startOffset: job.section.startOffset,
|
|
423
|
+
slug: job.slug,
|
|
275
424
|
kind,
|
|
276
|
-
proposedDecTitle: typeof r["proposed_dec_title"] === "string" ? r["proposed_dec_title"] : "",
|
|
277
|
-
proposedRationale: typeof r["proposed_rationale"] === "string" ? r["proposed_rationale"] : "",
|
|
278
|
-
conflictsWith: typeof r["conflicts_with"] === "string" ? r["conflicts_with"] : "",
|
|
279
425
|
failed: false,
|
|
280
426
|
};
|
|
281
427
|
}
|
|
282
428
|
catch (err) {
|
|
283
|
-
return informational(
|
|
284
|
-
source: source.path,
|
|
285
|
-
section,
|
|
286
|
-
failed: true,
|
|
287
|
-
errorMessage: err instanceof Error ? err.message : String(err),
|
|
288
|
-
});
|
|
429
|
+
return informational(job, true, err instanceof Error ? err.message : String(err));
|
|
289
430
|
}
|
|
290
431
|
}
|
|
291
|
-
function informational(
|
|
432
|
+
function informational(job, failed, errorMessage) {
|
|
292
433
|
return {
|
|
293
|
-
source:
|
|
294
|
-
level:
|
|
295
|
-
title:
|
|
296
|
-
startOffset:
|
|
434
|
+
source: job.source.path,
|
|
435
|
+
level: job.section.level,
|
|
436
|
+
title: job.section.title,
|
|
437
|
+
startOffset: job.section.startOffset,
|
|
438
|
+
slug: job.slug,
|
|
297
439
|
kind: "informational",
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
conflictsWith: "",
|
|
301
|
-
failed: args.failed,
|
|
302
|
-
...(args.errorMessage !== undefined ? { errorMessage: args.errorMessage } : {}),
|
|
440
|
+
failed,
|
|
441
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
303
442
|
};
|
|
304
443
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
const
|
|
444
|
+
function isEmittableKind(kind) {
|
|
445
|
+
return kind === "decision" || kind === "domain-rule" || kind === "constraint";
|
|
446
|
+
}
|
|
447
|
+
function jaccardCandidates(args) {
|
|
448
|
+
const newTokens = tokenize(args.newBody, { codeAware: true });
|
|
449
|
+
const scored = [];
|
|
450
|
+
for (const [id, entry] of Object.entries(args.cache.entries)) {
|
|
451
|
+
if (id === args.newId)
|
|
452
|
+
continue;
|
|
453
|
+
const candidateTokens = new Set(entry.tokens);
|
|
454
|
+
const score = jaccard(newTokens, candidateTokens);
|
|
455
|
+
if (score < args.threshold)
|
|
456
|
+
continue;
|
|
457
|
+
scored.push({ id, sot_path: entry.sot_path, similarity: score });
|
|
458
|
+
}
|
|
459
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
460
|
+
return scored.slice(0, args.topK);
|
|
461
|
+
}
|
|
462
|
+
async function runContradictionJudge(args) {
|
|
463
|
+
if (args.mock !== undefined) {
|
|
464
|
+
const verdict = await args.mock({
|
|
465
|
+
newBody: args.newBody,
|
|
466
|
+
candidateId: args.candidateId,
|
|
467
|
+
candidateBody: args.candidateBody,
|
|
468
|
+
});
|
|
469
|
+
return { verdict, reasoning: `(mock judge → ${verdict})` };
|
|
470
|
+
}
|
|
471
|
+
const a = capBody(args.newBody);
|
|
472
|
+
const b = capBody(args.candidateBody);
|
|
473
|
+
const prompt = [
|
|
474
|
+
"Statement A (newly captured by phase 7c):",
|
|
475
|
+
a,
|
|
476
|
+
"",
|
|
477
|
+
`Statement B (already accepted as ${args.candidateId}):`,
|
|
478
|
+
b,
|
|
479
|
+
"",
|
|
480
|
+
"Do these statements logically contradict each other?",
|
|
481
|
+
].join("\n");
|
|
482
|
+
try {
|
|
483
|
+
const result = await runClaude({
|
|
484
|
+
tier: "haiku",
|
|
485
|
+
system: CONTRADICTION_SYSTEM,
|
|
486
|
+
prompt,
|
|
487
|
+
jsonSchema: CONTRADICTION_SCHEMA,
|
|
488
|
+
timeoutMs: PER_CONTRADICTION_TIMEOUT_MS,
|
|
489
|
+
isolateAmbientContext: true,
|
|
490
|
+
});
|
|
491
|
+
const parsed = result.parsed;
|
|
492
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
493
|
+
return { verdict: "unrelated", reasoning: "(non-object judge response)" };
|
|
494
|
+
}
|
|
495
|
+
const r = parsed;
|
|
496
|
+
const verdictRaw = r["verdict"];
|
|
497
|
+
const verdict = verdictRaw === "contradict" || verdictRaw === "agree"
|
|
498
|
+
? verdictRaw
|
|
499
|
+
: "unrelated";
|
|
500
|
+
const reasoning = typeof r["reasoning"] === "string" ? r["reasoning"] : "";
|
|
501
|
+
return { verdict, reasoning };
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
log.warn({
|
|
505
|
+
candidateId: args.candidateId,
|
|
506
|
+
err: err instanceof Error ? err.message : String(err),
|
|
507
|
+
}, "contradiction judge failed; treating as unrelated");
|
|
508
|
+
return { verdict: "unrelated", reasoning: "(judge failed)" };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function capBody(body) {
|
|
512
|
+
return body.length > CONFLICT_BODY_CAP
|
|
513
|
+
? `${body.slice(0, CONFLICT_BODY_CAP)}\n…[truncated]`
|
|
514
|
+
: body;
|
|
515
|
+
}
|
|
516
|
+
function writeConflictFile(args) {
|
|
517
|
+
const dir = conflictsDir(args.repoRoot);
|
|
518
|
+
mkdirSync(dir, { recursive: true });
|
|
519
|
+
const filename = `${args.newId}__${args.otherId}.md`;
|
|
520
|
+
const abs = join(dir, filename);
|
|
521
|
+
const rel = `.cairn/ground/conflicts/${filename}`;
|
|
312
522
|
const fm = {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
capture_source: "init-rules-merge",
|
|
323
|
-
capture_confidence: "medium",
|
|
324
|
-
sourceFile: args.classification.source,
|
|
325
|
-
sectionTitle: args.classification.title,
|
|
523
|
+
a_id: args.newId,
|
|
524
|
+
a_source: args.newSourceFile,
|
|
525
|
+
a_capture_source: CAPTURE_SOURCE,
|
|
526
|
+
b_id: args.otherId,
|
|
527
|
+
b_sot_path: args.otherSotPath,
|
|
528
|
+
detected_at: args.generatedAt,
|
|
529
|
+
detector: "phase-7c-contradiction-judge",
|
|
530
|
+
severity: "soft",
|
|
531
|
+
reasoning: args.reasoning,
|
|
326
532
|
};
|
|
327
533
|
const lines = [];
|
|
328
534
|
lines.push("---");
|
|
329
535
|
lines.push(stringifyYaml(fm).trimEnd());
|
|
330
536
|
lines.push("---");
|
|
331
537
|
lines.push("");
|
|
332
|
-
lines.push(`# ${args.
|
|
538
|
+
lines.push(`# Conflict — ${args.newId} vs ${args.otherId}`);
|
|
333
539
|
lines.push("");
|
|
334
|
-
lines.push(
|
|
540
|
+
lines.push(`## ${args.newId} (just captured from \`${args.newSourceFile}\`)`);
|
|
335
541
|
lines.push("");
|
|
336
|
-
lines.push(
|
|
542
|
+
lines.push("```");
|
|
543
|
+
lines.push(args.newBody.trimEnd());
|
|
544
|
+
lines.push("```");
|
|
337
545
|
lines.push("");
|
|
338
|
-
lines.push(
|
|
546
|
+
lines.push(`## ${args.otherId} (already accepted, sot_path: \`${args.otherSotPath}\`)`);
|
|
339
547
|
lines.push("");
|
|
340
|
-
lines.push(
|
|
548
|
+
lines.push("```");
|
|
549
|
+
lines.push(args.otherBody.trimEnd());
|
|
550
|
+
lines.push("```");
|
|
551
|
+
lines.push("");
|
|
552
|
+
lines.push("## Judge reasoning");
|
|
553
|
+
lines.push("");
|
|
554
|
+
lines.push(args.reasoning.trim().length > 0 ? args.reasoning.trim() : "(no reasoning provided)");
|
|
341
555
|
lines.push("");
|
|
342
556
|
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
343
|
-
return
|
|
557
|
+
return rel;
|
|
558
|
+
}
|
|
559
|
+
function readEmittedBody(repoRoot, id) {
|
|
560
|
+
const isDec = id.startsWith("DEC-");
|
|
561
|
+
const dir = isDec
|
|
562
|
+
? join(repoRoot, ".cairn", "ground", "decisions")
|
|
563
|
+
: join(repoRoot, ".cairn", "ground", "invariants");
|
|
564
|
+
const abs = join(dir, `${id}.md`);
|
|
565
|
+
if (!existsSync(abs))
|
|
566
|
+
return null;
|
|
567
|
+
let raw;
|
|
568
|
+
try {
|
|
569
|
+
raw = readFileSync(abs, "utf8");
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
// Strip frontmatter — body is everything past the second `---` line.
|
|
575
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
576
|
+
if (fmMatch === null)
|
|
577
|
+
return raw.trim();
|
|
578
|
+
return raw.slice(fmMatch[0].length).trim();
|
|
579
|
+
}
|
|
580
|
+
function persistGroundState(args) {
|
|
581
|
+
const { repoRoot } = args;
|
|
582
|
+
// Re-read each ground-state file right before write so concurrent
|
|
583
|
+
// writers (phase 6 / 7b) don't get clobbered. parallel-678 runs the
|
|
584
|
+
// three phases sequentially under v0.5.0; this merge is defense in
|
|
585
|
+
// depth for the individual phase tools.
|
|
586
|
+
const freshTopic = readTopicIndex(repoRoot);
|
|
587
|
+
const baseTopic = Object.keys(freshTopic.topics).length > 0 ? freshTopic : emptyTopicIndex();
|
|
588
|
+
for (const [slug, entry] of Object.entries(args.topicIndex.topics)) {
|
|
589
|
+
baseTopic.topics[slug] = entry;
|
|
590
|
+
}
|
|
591
|
+
baseTopic.generated = new Date().toISOString();
|
|
592
|
+
writeTopicIndex(repoRoot, baseTopic);
|
|
593
|
+
const freshAnchor = readAnchorMap(repoRoot);
|
|
594
|
+
const baseAnchor = Object.keys(freshAnchor.anchors).length > 0 ? freshAnchor : emptyAnchorMap();
|
|
595
|
+
for (const [slug, anchor] of Object.entries(args.anchorMap.anchors)) {
|
|
596
|
+
baseAnchor.anchors[slug] = anchor;
|
|
597
|
+
}
|
|
598
|
+
baseAnchor.generated = new Date().toISOString();
|
|
599
|
+
writeAnchorMap(repoRoot, baseAnchor);
|
|
600
|
+
const freshBindings = readSotBindings(repoRoot);
|
|
601
|
+
const baseBindings = Object.keys(freshBindings.forward).length > 0 ? freshBindings : emptySotBindings();
|
|
602
|
+
for (const [decId, sotPath] of Object.entries(args.bindings.forward)) {
|
|
603
|
+
baseBindings.forward[decId] = sotPath;
|
|
604
|
+
}
|
|
605
|
+
for (const [sotPath, decIds] of Object.entries(args.bindings.reverse)) {
|
|
606
|
+
const seen = new Set(baseBindings.reverse[sotPath] ?? []);
|
|
607
|
+
for (const id of decIds)
|
|
608
|
+
seen.add(id);
|
|
609
|
+
baseBindings.reverse[sotPath] = Array.from(seen);
|
|
610
|
+
}
|
|
611
|
+
baseBindings.generated = new Date().toISOString();
|
|
612
|
+
writeSotBindings(repoRoot, baseBindings);
|
|
613
|
+
const freshCache = readSotCache(repoRoot);
|
|
614
|
+
let baseCache = Object.keys(freshCache.entries).length > 0 ? freshCache : emptySotCache();
|
|
615
|
+
for (const [decId, entry] of Object.entries(args.cache.entries)) {
|
|
616
|
+
baseCache = setSotCacheEntry(baseCache, decId, entry);
|
|
617
|
+
}
|
|
618
|
+
baseCache.generated = new Date().toISOString();
|
|
619
|
+
writeSotCache(repoRoot, baseCache);
|
|
620
|
+
}
|
|
621
|
+
function stripLeadingHeading(body) {
|
|
622
|
+
// parseRuleSections always pushes the heading line as the first entry
|
|
623
|
+
// of `body`; strip it so the slug + emitted DEC body match phase 5b's
|
|
624
|
+
// section fingerprint convention (heading excluded from fingerprint).
|
|
625
|
+
const newlineIdx = body.indexOf("\n");
|
|
626
|
+
const trimmedFirst = body.slice(0, newlineIdx === -1 ? body.length : newlineIdx).trim();
|
|
627
|
+
if (trimmedFirst.startsWith("#")) {
|
|
628
|
+
return body.slice(newlineIdx === -1 ? body.length : newlineIdx + 1).trim();
|
|
629
|
+
}
|
|
630
|
+
return body.trim();
|
|
631
|
+
}
|
|
632
|
+
function headingToAnchor(line) {
|
|
633
|
+
return line
|
|
634
|
+
.toLowerCase()
|
|
635
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
636
|
+
.trim()
|
|
637
|
+
.replace(/\s+/g, "-");
|
|
344
638
|
}
|
|
345
639
|
function writeYaml(path, payload) {
|
|
346
640
|
mkdirSync(dirname(path), { recursive: true });
|