@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,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,46 +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
|
-
|
|
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 };
|
|
146
226
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,
|
|
152
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
|
+
}
|
|
153
307
|
}
|
|
154
308
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
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
|
+
}
|
|
162
327
|
}
|
|
163
328
|
}
|
|
329
|
+
// ── 8. Audit yaml ───────────────────────────────────────────────
|
|
164
330
|
const auditRelPath = `.cairn/baseline/rules-merge-${tsSlug}.yaml`;
|
|
165
331
|
const auditPath = join(repoRoot, auditRelPath);
|
|
166
|
-
let conflictsPath = null;
|
|
167
332
|
const kindCounts = {
|
|
168
|
-
|
|
169
|
-
"rule
|
|
333
|
+
decision: 0,
|
|
334
|
+
"domain-rule": 0,
|
|
335
|
+
constraint: 0,
|
|
170
336
|
informational: 0,
|
|
171
337
|
"operator-keep": 0,
|
|
172
338
|
};
|
|
@@ -179,58 +345,52 @@ export async function runRulesMerge(args) {
|
|
|
179
345
|
sources: sources.map((s) => ({ path: s.path, kind: s.kind, size: s.size })),
|
|
180
346
|
sections_total: allClassifications.length,
|
|
181
347
|
kind_counts: kindCounts,
|
|
348
|
+
decs_written: decsWritten.length,
|
|
349
|
+
invs_written: invsWritten.length,
|
|
350
|
+
cites_emitted: citesEmitted.length,
|
|
351
|
+
conflicts: conflicts.length,
|
|
182
352
|
classifications: allClassifications.map((c) => ({
|
|
183
353
|
source: c.source,
|
|
184
354
|
title: c.title,
|
|
185
355
|
level: c.level,
|
|
186
356
|
kind: c.kind,
|
|
357
|
+
slug: c.slug,
|
|
187
358
|
start_offset: c.startOffset,
|
|
188
|
-
proposed_dec_title: c.proposedDecTitle,
|
|
189
|
-
proposed_rationale: c.proposedRationale,
|
|
190
|
-
conflicts_with: c.conflictsWith,
|
|
191
359
|
failed: c.failed,
|
|
192
360
|
...(c.errorMessage !== undefined ? { error: c.errorMessage } : {}),
|
|
193
361
|
})),
|
|
194
362
|
});
|
|
195
|
-
if (conflictRows.length > 0) {
|
|
196
|
-
const rel = `.cairn/baseline/rule-conflicts-${tsSlug}.yaml`;
|
|
197
|
-
conflictsPath = join(repoRoot, rel);
|
|
198
|
-
writeYaml(conflictsPath, {
|
|
199
|
-
run_at: nowIso,
|
|
200
|
-
conflicts: conflictRows,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
363
|
}
|
|
204
364
|
log.info({
|
|
205
365
|
sources: sources.length,
|
|
206
366
|
sections: allClassifications.length,
|
|
207
367
|
kindCounts,
|
|
208
|
-
|
|
209
|
-
|
|
368
|
+
decs: decsWritten.length,
|
|
369
|
+
invs: invsWritten.length,
|
|
370
|
+
cites: citesEmitted.length,
|
|
371
|
+
conflicts: conflicts.length,
|
|
210
372
|
}, "rules merge complete");
|
|
211
373
|
return {
|
|
212
374
|
sources,
|
|
213
375
|
sectionsTotal: allClassifications.length,
|
|
214
376
|
classifications: allClassifications,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
377
|
+
decsWritten,
|
|
378
|
+
invsWritten,
|
|
379
|
+
citesEmitted,
|
|
380
|
+
conflicts,
|
|
218
381
|
auditPath,
|
|
219
382
|
auditRelPath,
|
|
220
383
|
kindCounts,
|
|
221
384
|
};
|
|
222
385
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const body = section.body.length > SECTION_BODY_CAP
|
|
228
|
-
? `${section.body.slice(0, SECTION_BODY_CAP)}\n…[truncated]`
|
|
229
|
-
: 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;
|
|
230
390
|
const prompt = [
|
|
231
|
-
`Source: ${source.path}`,
|
|
232
|
-
`Section title: ${section.title || "(preamble)"}`,
|
|
233
|
-
`Heading level: ${section.level}`,
|
|
391
|
+
`Source: ${job.source.path}`,
|
|
392
|
+
`Section title: ${job.section.title || "(preamble)"}`,
|
|
393
|
+
`Heading level: ${job.section.level}`,
|
|
234
394
|
"",
|
|
235
395
|
"Body:",
|
|
236
396
|
body,
|
|
@@ -246,94 +406,235 @@ async function classifySection(source, section) {
|
|
|
246
406
|
});
|
|
247
407
|
const parsed = result.parsed;
|
|
248
408
|
if (typeof parsed !== "object" || parsed === null) {
|
|
249
|
-
return informational(
|
|
250
|
-
source: source.path,
|
|
251
|
-
section,
|
|
252
|
-
failed: true,
|
|
253
|
-
errorMessage: "non-object response",
|
|
254
|
-
});
|
|
409
|
+
return informational(job, true, "non-object response");
|
|
255
410
|
}
|
|
256
411
|
const r = parsed;
|
|
257
412
|
const kindRaw = r["kind"];
|
|
258
|
-
const kind = kindRaw === "
|
|
259
|
-
kindRaw === "rule
|
|
260
|
-
kindRaw === "
|
|
413
|
+
const kind = kindRaw === "decision" ||
|
|
414
|
+
kindRaw === "domain-rule" ||
|
|
415
|
+
kindRaw === "constraint"
|
|
261
416
|
? kindRaw
|
|
262
417
|
: "informational";
|
|
263
418
|
return {
|
|
264
|
-
source: source.path,
|
|
265
|
-
level: section.level,
|
|
266
|
-
title: section.title,
|
|
267
|
-
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,
|
|
268
424
|
kind,
|
|
269
|
-
proposedDecTitle: typeof r["proposed_dec_title"] === "string" ? r["proposed_dec_title"] : "",
|
|
270
|
-
proposedRationale: typeof r["proposed_rationale"] === "string" ? r["proposed_rationale"] : "",
|
|
271
|
-
conflictsWith: typeof r["conflicts_with"] === "string" ? r["conflicts_with"] : "",
|
|
272
425
|
failed: false,
|
|
273
426
|
};
|
|
274
427
|
}
|
|
275
428
|
catch (err) {
|
|
276
|
-
return informational(
|
|
277
|
-
source: source.path,
|
|
278
|
-
section,
|
|
279
|
-
failed: true,
|
|
280
|
-
errorMessage: err instanceof Error ? err.message : String(err),
|
|
281
|
-
});
|
|
429
|
+
return informational(job, true, err instanceof Error ? err.message : String(err));
|
|
282
430
|
}
|
|
283
431
|
}
|
|
284
|
-
function informational(
|
|
432
|
+
function informational(job, failed, errorMessage) {
|
|
285
433
|
return {
|
|
286
|
-
source:
|
|
287
|
-
level:
|
|
288
|
-
title:
|
|
289
|
-
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,
|
|
290
439
|
kind: "informational",
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
conflictsWith: "",
|
|
294
|
-
failed: args.failed,
|
|
295
|
-
...(args.errorMessage !== undefined ? { errorMessage: args.errorMessage } : {}),
|
|
440
|
+
failed,
|
|
441
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
296
442
|
};
|
|
297
443
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
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}`;
|
|
305
522
|
const fm = {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
capture_source: "init-rules-merge",
|
|
316
|
-
capture_confidence: "medium",
|
|
317
|
-
sourceFile: args.classification.source,
|
|
318
|
-
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,
|
|
319
532
|
};
|
|
320
533
|
const lines = [];
|
|
321
534
|
lines.push("---");
|
|
322
535
|
lines.push(stringifyYaml(fm).trimEnd());
|
|
323
536
|
lines.push("---");
|
|
324
537
|
lines.push("");
|
|
325
|
-
lines.push(`# ${args.
|
|
538
|
+
lines.push(`# Conflict — ${args.newId} vs ${args.otherId}`);
|
|
326
539
|
lines.push("");
|
|
327
|
-
lines.push(
|
|
540
|
+
lines.push(`## ${args.newId} (just captured from \`${args.newSourceFile}\`)`);
|
|
328
541
|
lines.push("");
|
|
329
|
-
lines.push(
|
|
542
|
+
lines.push("```");
|
|
543
|
+
lines.push(args.newBody.trimEnd());
|
|
544
|
+
lines.push("```");
|
|
330
545
|
lines.push("");
|
|
331
|
-
lines.push(
|
|
546
|
+
lines.push(`## ${args.otherId} (already accepted, sot_path: \`${args.otherSotPath}\`)`);
|
|
332
547
|
lines.push("");
|
|
333
|
-
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)");
|
|
334
555
|
lines.push("");
|
|
335
556
|
writeFileSync(abs, lines.join("\n"), "utf8");
|
|
336
|
-
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, "-");
|
|
337
638
|
}
|
|
338
639
|
function writeYaml(path, payload) {
|
|
339
640
|
mkdirSync(dirname(path), { recursive: true });
|