@smartmemory/compose 0.3.5-beta → 0.3.7
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/.claude/skills/compose/SKILL.md +42 -88
- package/bin/compose.js +288 -0
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +572 -0
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/judgment-record.schema.json +821 -0
- package/dist/assets/App-PkZzHeMj.js +894 -0
- package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
- package/dist/assets/arc-BafGpyqE.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
- package/dist/assets/channel-qVK_qn4E.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
- package/dist/assets/clone-Pu3RyLUh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
- package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
- package/dist/assets/graph-Bi99_6Yf.js +331 -0
- package/dist/assets/graph-RE4I7Ty7.js +1 -0
- package/dist/assets/index-Rm2RE-c0.js +123 -0
- package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BSf33zm8.js +1 -0
- package/dist/assets/{linear-CsXqv70E.js → linear-AvSTWMqx.js} +1 -1
- package/dist/assets/min-QBM8H4xN.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
- package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
- package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
- package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
- package/dist/index.html +2 -2
- package/lib/bug-escalation.js +30 -4
- package/lib/build.js +777 -52
- package/lib/canon-guard.js +223 -0
- package/lib/canon-registry.js +187 -0
- package/lib/codex-preflight.js +26 -4
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-writer.js +9 -0
- package/lib/gsd.js +11 -2
- package/lib/hooks-status.js +32 -3
- package/lib/idempotency.js +33 -8
- package/lib/judgment/schema.js +23 -0
- package/lib/judgment/store/index.js +207 -0
- package/lib/judgment/store/records.js +405 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-gen.js +691 -0
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-write-guard.js +230 -0
- package/lib/judgment-writer.js +3403 -0
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +42 -0
- package/lib/local-claude-connector.js +199 -54
- package/lib/mcp-enforcement.js +21 -35
- package/lib/result-normalizer.js +93 -15
- package/lib/review-normalize.js +4 -0
- package/lib/stratum-mcp-client.js +131 -6
- package/package.json +2 -2
- package/server/compose-mcp-tools.js +49 -1
- package/server/compose-mcp.js +220 -0
- package/server/mcp-tool-policy.js +1 -0
- package/dist/assets/App-BG92y_CY.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-Bip6cMjP.js +0 -1
- package/dist/assets/arc-CyMTM07c.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-BukV0CJw.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-Bjb_MNB4.js +0 -132
- package/dist/assets/channel-BzkmtaLy.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-G_MZEji_.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-CQZQYMcz.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-CmAda4sZ.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-CbjBz-Ek.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-C3n1PNJC.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-C8N_CvKJ.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-B2pkwDhN.js +0 -4
- package/dist/assets/diagram-FQU43EPY-DPHD_t-1.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DGmHIZRr.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BlLWwKVt.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-DEv704Re.js +0 -30
- package/dist/assets/diagram-WEI45ONY-Bj6PGUCe.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-Ch-icksT.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-CI3lwEFA.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DcUANpAz.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-Cy0Ie8KS.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-BswI52az.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-B1H7-G2y.js +0 -119
- package/dist/assets/infoDiagram-FWYZ7A6U-BBunqvr-.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/pegDiagram-2B236MQR-HpQyrDcm.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-xkcpHwnY.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-kebZIOmu.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-DNvGwWm9.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-DOEEcn52.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DynIVCk_.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DJXFjUUL.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-OZ7trFh5.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-DULYAFmB.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-CdKmtcFL.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-DknGwjCA.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DdYyKfQ3.js +0 -7
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/judgment-import.js — one-time markdown → records import (S07/W3).
|
|
4
|
+
*
|
|
5
|
+
* Parses the hand-written judgment canon (REGISTER.md, LEDGER.md,
|
|
6
|
+
* OBJECTIVE.md, positions/*.md) into records and writes them THROUGH the
|
|
7
|
+
* writer with `via: 'import'` — historical transcription, not new
|
|
8
|
+
* authorship: original dates and `[ASSERT]` grounding are preserved
|
|
9
|
+
* (ASSERT claims gain a citation-elicitation pointing at the imported
|
|
10
|
+
* source), `minutes` costs map to `hours` with a note flag, and curated
|
|
11
|
+
* banners/prose become `note` records with placement anchors.
|
|
12
|
+
*
|
|
13
|
+
* Ledger entries that predate the schema's kind-specific requirements and
|
|
14
|
+
* cannot be mapped losslessly (e.g. the slot-assignment `rank` entry) fall
|
|
15
|
+
* back to `note` records that keep the original heading in the title —
|
|
16
|
+
* deterministic, honest, and reviewable in the cutover diff.
|
|
17
|
+
*
|
|
18
|
+
* Dry-run stages the import in a temp workspace and diffs the regenerated
|
|
19
|
+
* projections against the current hand-written files; the real run is
|
|
20
|
+
* refused if records already exist (import is one-time). Kept for provider
|
|
21
|
+
* migrations (PROVIDER-SEAM: switching is a re-import, never a sync).
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, readdirSync, existsSync, mkdtempSync } from 'node:fs';
|
|
24
|
+
import { join, basename } from 'node:path';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
judgmentPositionCreate,
|
|
29
|
+
judgmentJointAdd,
|
|
30
|
+
judgmentLedgerAppend,
|
|
31
|
+
} from '../lib/judgment-writer.js';
|
|
32
|
+
import {
|
|
33
|
+
computeRecordHashes,
|
|
34
|
+
writeManifest,
|
|
35
|
+
} from '../lib/judgment-attest.js';
|
|
36
|
+
import { getJudgmentValidator } from '../lib/judgment/schema.js';
|
|
37
|
+
|
|
38
|
+
const METHODS = ['EXT', 'INT', 'CONSTRUCT', 'ASSERT', 'STRADDLE'];
|
|
39
|
+
const RULED_COSTS = ['hours', 'days', 'weeks', 'months'];
|
|
40
|
+
const DEFAULT_DATE = '2026-07-20';
|
|
41
|
+
|
|
42
|
+
/** Map a raw cost cell onto the ruled COARSE-BUCKETS. */
|
|
43
|
+
export function mapCost(raw) {
|
|
44
|
+
const cost = String(raw ?? '').toLowerCase().trim();
|
|
45
|
+
if (RULED_COSTS.includes(cost)) return { cost, note: null };
|
|
46
|
+
if (cost === 'minutes') return { cost: 'hours', note: 'import: cost "minutes" mapped to "hours" (COARSE-BUCKETS)' };
|
|
47
|
+
return { cost: 'hours', note: `import: cost "${raw}" is not a ruled bucket — defaulted to "hours"` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Shared parsing helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function cells(line) {
|
|
55
|
+
return line.replace(/^\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function plain(cell) {
|
|
59
|
+
return cell.replace(/\*\*/g, '').replace(/`/g, '').replace(/~~/g, '').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ts(date) {
|
|
63
|
+
return `${date}T12:00:00Z`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function convictionFrom(text) {
|
|
67
|
+
const t = (text ?? '').toLowerCase();
|
|
68
|
+
const level = ['high', 'medium', 'low'].find((l) => t.includes(l)) ?? 'medium';
|
|
69
|
+
const source = t.includes('inferred') ? 'inferred' : (t.includes('stated') || t.includes('owner') ? 'stated' : 'inferred');
|
|
70
|
+
return { level, source };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTableRow(line) {
|
|
74
|
+
return line.startsWith('|') && !/^\|[\s\-:|]+\|?\s*$/.test(line);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// REGISTER.md → joints + anchored notes
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function parseRegister(text, objectiveQuestions) {
|
|
82
|
+
const lines = text.split('\n');
|
|
83
|
+
const joints = [];
|
|
84
|
+
const notes = [];
|
|
85
|
+
const struckRows = new Map(); // slug → question (for dissolved joints)
|
|
86
|
+
|
|
87
|
+
let section = 'banner';
|
|
88
|
+
const bannerLines = [];
|
|
89
|
+
const footerBlocks = []; // { title, body[] }
|
|
90
|
+
let currentFooter = null;
|
|
91
|
+
|
|
92
|
+
const sectionOf = (heading) => {
|
|
93
|
+
if (/^## Under test/.test(heading)) return { name: 'table', state: 'under_test', rank: 'high' };
|
|
94
|
+
if (/^## Open — high/.test(heading)) return { name: 'table', state: 'open', rank: 'high' };
|
|
95
|
+
if (/^## Open — medium/.test(heading)) return { name: 'table', state: 'open', rank: 'medium' };
|
|
96
|
+
if (/^## Resolved/.test(heading)) return { name: 'resolved' };
|
|
97
|
+
if (/^## Dissolved/.test(heading)) return { name: 'dissolved' };
|
|
98
|
+
return { name: 'footer', title: heading.replace(/^##\s*/, '') };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let mode = { name: 'banner' };
|
|
102
|
+
let dissolvedBody = [];
|
|
103
|
+
|
|
104
|
+
for (const raw of lines) {
|
|
105
|
+
const line = raw.trimEnd();
|
|
106
|
+
if (/^# /.test(line)) continue;
|
|
107
|
+
if (/^## /.test(line)) {
|
|
108
|
+
mode = sectionOf(line);
|
|
109
|
+
if (mode.name === 'footer') {
|
|
110
|
+
currentFooter = { title: mode.title, body: [] };
|
|
111
|
+
footerBlocks.push(currentFooter);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (mode.name === 'banner') {
|
|
116
|
+
bannerLines.push(line);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (mode.name === 'footer') {
|
|
120
|
+
currentFooter.body.push(line);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (mode.name === 'dissolved') {
|
|
124
|
+
dissolvedBody.push(line);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (mode.name === 'table' || mode.name === 'resolved') {
|
|
128
|
+
if (!isTableRow(line)) {
|
|
129
|
+
// inter-table prose (Moved here…, Re-weighted…, demotion notes)
|
|
130
|
+
if (line.trim()) footerBlocks.push({ title: 'register prose', body: [line] });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const row = cells(line);
|
|
134
|
+
if (row.length < 4 || plain(row[0]).toLowerCase() === 'joint') continue;
|
|
135
|
+
if (raw.includes('~~')) {
|
|
136
|
+
struckRows.set(plain(row[0]), plain(row[1]));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (mode.name === 'resolved') {
|
|
140
|
+
const [slugCell, dateCell, byCell, outcomeCell] = row;
|
|
141
|
+
const slug = plain(slugCell);
|
|
142
|
+
const date = plain(dateCell);
|
|
143
|
+
const question = objectiveQuestions.get(slug) ?? '(question not recorded at import)';
|
|
144
|
+
const refMatch = /\(`?(decide: [a-z0-9-]+)`?\)/.exec(outcomeCell);
|
|
145
|
+
const { cost, note } = mapCost('minutes'); // the register records these as minutes-scale ASSERT rulings
|
|
146
|
+
joints.push({
|
|
147
|
+
date,
|
|
148
|
+
record: {
|
|
149
|
+
slug,
|
|
150
|
+
question,
|
|
151
|
+
branch_true: '(not recorded at import)',
|
|
152
|
+
branch_false: '(not recorded at import)',
|
|
153
|
+
resolve_by: METHODS.find((m) => byCell.includes(m)) ?? 'ASSERT',
|
|
154
|
+
cost,
|
|
155
|
+
rank: 'medium',
|
|
156
|
+
state: 'resolved',
|
|
157
|
+
flags: [note].filter(Boolean),
|
|
158
|
+
resolution: {
|
|
159
|
+
outcome: 'resolved',
|
|
160
|
+
evidence: plain(outcomeCell),
|
|
161
|
+
elicitation: {
|
|
162
|
+
asked: question,
|
|
163
|
+
answered_at: ts(date),
|
|
164
|
+
answer_ref: refMatch ? refMatch[1] : 'docs/judgment/LEDGER.md',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// open / under_test tables
|
|
172
|
+
const [slugCell, qCell, tCell, fCell, byCell, costCell] = row;
|
|
173
|
+
const slug = plain(slugCell);
|
|
174
|
+
// Strip flag tokens BEFORE method detection — 'EXT-UNREACHABLE' must not
|
|
175
|
+
// read as method EXT (already-knew resolves by INT).
|
|
176
|
+
const byClean = plain(byCell ?? '').replace(/EXT-UNREACHABLE/g, '').replace(/blocked on sharpening/gi, '');
|
|
177
|
+
const method = (/\b(EXT|INT|CONSTRUCT|ASSERT|STRADDLE)\b/.exec(byClean) ?? [])[1] ?? 'INT';
|
|
178
|
+
const { cost, note } = mapCost(plain(costCell ?? ''));
|
|
179
|
+
const flags = [];
|
|
180
|
+
if (/EXT-UNREACHABLE/.test(byCell ?? '')) flags.push('EXT-UNREACHABLE');
|
|
181
|
+
if (/blocked on sharpening/i.test(byCell ?? '')) flags.push('blocked-on-sharpening');
|
|
182
|
+
const methodNote = byClean
|
|
183
|
+
.replace(new RegExp(`\\b${method}\\b`), '')
|
|
184
|
+
.replace(/\s{2,}/g, ' ')
|
|
185
|
+
.replace(/^[\s—·-]+|[\s—·-]+$/g, '');
|
|
186
|
+
if (methodNote) flags.push(`method-note: ${methodNote}`);
|
|
187
|
+
if (note) flags.push(note);
|
|
188
|
+
joints.push({
|
|
189
|
+
date: DEFAULT_DATE,
|
|
190
|
+
record: {
|
|
191
|
+
slug,
|
|
192
|
+
question: plain(qCell),
|
|
193
|
+
branch_true: plain(tCell),
|
|
194
|
+
branch_false: plain(fCell),
|
|
195
|
+
resolve_by: method,
|
|
196
|
+
cost,
|
|
197
|
+
rank: mode.rank,
|
|
198
|
+
state: mode.state,
|
|
199
|
+
flags,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Dissolved joints from struck rows + the Dissolved section prose.
|
|
206
|
+
const dissolvedText = dissolvedBody.join('\n');
|
|
207
|
+
for (const [slug, question] of struckRows) {
|
|
208
|
+
const halves = [...new Set([...dissolvedText.matchAll(/\bthe ([A-Z][a-z]+)\b/g)].map((m) => m[1].toLowerCase()))];
|
|
209
|
+
joints.push({
|
|
210
|
+
date: DEFAULT_DATE,
|
|
211
|
+
record: {
|
|
212
|
+
slug,
|
|
213
|
+
question: question || '(question not recorded at import)',
|
|
214
|
+
branch_true: '(not recorded at import)',
|
|
215
|
+
branch_false: '(not recorded at import)',
|
|
216
|
+
resolve_by: 'CONSTRUCT',
|
|
217
|
+
cost: 'hours',
|
|
218
|
+
rank: 'medium',
|
|
219
|
+
state: 'dissolved',
|
|
220
|
+
flags: ['import: resolve_by/cost not recorded on the dissolved row — defaulted'],
|
|
221
|
+
dissolution: { decomposed_into: halves.length ? halves : ['recorded-in-dissolution-note'] },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
notes.push({
|
|
227
|
+
date: '2026-07-22',
|
|
228
|
+
event: { kind: 'note', title: 'Register banner (imported)', body: bannerLines.filter(Boolean).join('\n'), anchor: 'register-header' },
|
|
229
|
+
});
|
|
230
|
+
if (dissolvedText.trim()) {
|
|
231
|
+
footerBlocks.push({ title: 'Dissolved', body: dissolvedBody });
|
|
232
|
+
}
|
|
233
|
+
for (const block of footerBlocks) {
|
|
234
|
+
const body = block.body.filter(Boolean).join('\n').trim();
|
|
235
|
+
if (!body) continue;
|
|
236
|
+
notes.push({
|
|
237
|
+
date: '2026-07-22',
|
|
238
|
+
event: { kind: 'note', title: `Register: ${block.title} (imported)`, body, anchor: 'register-footer' },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return { joints, notes };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// OBJECTIVE.md → position 'objective' + question map
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function parseObjective(text) {
|
|
249
|
+
const questions = new Map();
|
|
250
|
+
for (const line of text.split('\n')) {
|
|
251
|
+
if (!isTableRow(line)) continue;
|
|
252
|
+
const row = cells(line);
|
|
253
|
+
if (row.length >= 3 && /`[a-z0-9-]+`/.test(row[0])) {
|
|
254
|
+
questions.set(plain(row[0]), plain(row[1]));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const primary = /\*\*Primary:\*\*\s*([\s\S]*?)\n\n/.exec(text)?.[1]?.replace(/\n/g, ' ').trim()
|
|
258
|
+
?? '(objective not parseable at import)';
|
|
259
|
+
// The file's own header tags it `[ASSERT]` — preserved verbatim (binding:
|
|
260
|
+
// import preserves grounding). The elicitation citation records honestly
|
|
261
|
+
// that this was back-inferred, never elicited; the health warning survives
|
|
262
|
+
// in the anchored prose note.
|
|
263
|
+
const position = {
|
|
264
|
+
date: DEFAULT_DATE,
|
|
265
|
+
slug: 'objective',
|
|
266
|
+
record: {
|
|
267
|
+
slug: 'objective',
|
|
268
|
+
claims: [{
|
|
269
|
+
id: 'c1',
|
|
270
|
+
text: primary,
|
|
271
|
+
grounding: 'ASSERT',
|
|
272
|
+
supports: [],
|
|
273
|
+
elicitation: {
|
|
274
|
+
asked: 'What are we optimizing for?',
|
|
275
|
+
answered_at: ts(DEFAULT_DATE),
|
|
276
|
+
answer_ref: 'import:docs/judgment/OBJECTIVE.md (back-inferred draft — NOT owner-confirmed, see health warning)',
|
|
277
|
+
},
|
|
278
|
+
}],
|
|
279
|
+
conviction: { level: 'low', source: 'inferred' },
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
const note = {
|
|
283
|
+
date: DEFAULT_DATE,
|
|
284
|
+
event: { kind: 'note', title: 'OBJECTIVE.md (imported prose)', body: text.trim(), anchor: 'position:objective' },
|
|
285
|
+
};
|
|
286
|
+
return { position, note, questions };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// positions/*.md → position records + prose notes
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
function parsePositionFile(slug, text, sourceRel) {
|
|
294
|
+
const date = /\*\*Held since:\*\*\s*(\d{4}-\d{2}-\d{2})/.exec(text)?.[1]
|
|
295
|
+
?? /(\d{4}-\d{2}-\d{2})/.exec(text)?.[1] ?? DEFAULT_DATE;
|
|
296
|
+
const conviction = convictionFrom(/\*\*Conviction:\*\*([^\n]*)/.exec(text)?.[1] ?? '');
|
|
297
|
+
|
|
298
|
+
const claims = [];
|
|
299
|
+
const cite = (asked) => ({ asked, answered_at: ts(date), answer_ref: `import:${sourceRel}` });
|
|
300
|
+
|
|
301
|
+
// Numbered argument table: | # | Step | Grounding |
|
|
302
|
+
for (const line of text.split('\n')) {
|
|
303
|
+
if (!isTableRow(line)) continue;
|
|
304
|
+
const row = cells(line);
|
|
305
|
+
if (row.length >= 3 && /^\d+$/.test(plain(row[0]))) {
|
|
306
|
+
const id = `c${plain(row[0])}`;
|
|
307
|
+
const step = plain(row[1]);
|
|
308
|
+
const groundingCell = row[2];
|
|
309
|
+
const derivedFrom = /derived from (\d+)/i.exec(groundingCell);
|
|
310
|
+
let claim;
|
|
311
|
+
if (derivedFrom) {
|
|
312
|
+
claim = { id, text: step, grounding: 'DERIVED', supports: [`c${derivedFrom[1]}`] };
|
|
313
|
+
} else {
|
|
314
|
+
// Preserve the recorded grounding verbatim (binding); AGENT only when
|
|
315
|
+
// the cell names none of the vocabulary.
|
|
316
|
+
const grounding = (/\b(EXT|INT|ASSERT|DERIVED|AGENT)\b/.exec(groundingCell) ?? [])[1] ?? 'AGENT';
|
|
317
|
+
claim = { id, text: step, grounding, supports: [] };
|
|
318
|
+
if (grounding === 'ASSERT') claim.elicitation = cite(step);
|
|
319
|
+
}
|
|
320
|
+
if (/owner-locked/.test(groundingCell)) claim.owner_locked = true;
|
|
321
|
+
claims.push(claim);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Evidence bullets: - `[EXT]` text
|
|
325
|
+
if (claims.length === 0) {
|
|
326
|
+
let n = 0;
|
|
327
|
+
for (const line of text.split('\n')) {
|
|
328
|
+
const m = /^-\s*`\[(EXT|INT|ASSERT|AGENT|DERIVED)\]`\s*(.+)$/.exec(line.trim());
|
|
329
|
+
if (!m) continue;
|
|
330
|
+
n += 1;
|
|
331
|
+
const claim = { id: `c${n}`, text: plain(m[2]), grounding: m[1], supports: [] };
|
|
332
|
+
if (m[1] === 'ASSERT') claim.elicitation = cite(claim.text);
|
|
333
|
+
if (/owner-locked/.test(line)) claim.owner_locked = true;
|
|
334
|
+
claims.push(claim);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (claims.length === 0) {
|
|
338
|
+
const claimSection = /## (?:The )?[Cc]laim\n+([\s\S]*?)(?:\n## |$)/.exec(text)?.[1]?.replace(/\n/g, ' ').trim();
|
|
339
|
+
claims.push({ id: 'c1', text: claimSection ?? '(claim not parseable at import)', grounding: 'AGENT', supports: [] });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
position: { date, slug, record: { slug, claims, conviction } },
|
|
344
|
+
note: {
|
|
345
|
+
date,
|
|
346
|
+
event: { kind: 'note', title: `${basename(sourceRel)} (imported prose)`, body: text.trim(), anchor: `position:${slug}` },
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// LEDGER.md → ledger events (schema-mapped, note fallback)
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
const LEDGER_KINDS = ['decide', 'kill', 'override', 'escalate', 'calibrate', 'postmortem', 'rank', 'note', 'correct', 'open'];
|
|
356
|
+
|
|
357
|
+
function parseLedger(text) {
|
|
358
|
+
const lines = text.split('\n');
|
|
359
|
+
const entries = [];
|
|
360
|
+
let banner = [];
|
|
361
|
+
let sawSection = false;
|
|
362
|
+
let currentDate = DEFAULT_DATE;
|
|
363
|
+
let current = null;
|
|
364
|
+
|
|
365
|
+
const flush = () => {
|
|
366
|
+
if (current) entries.push(current);
|
|
367
|
+
current = null;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
for (const raw of lines) {
|
|
371
|
+
const line = raw.trimEnd();
|
|
372
|
+
const heading = /^###\s+([a-z]+):\s*(.+)$/.exec(line);
|
|
373
|
+
if (/^## /.test(line)) {
|
|
374
|
+
flush();
|
|
375
|
+
sawSection = true;
|
|
376
|
+
const date = /(\d{4}-\d{2}-\d{2})/.exec(line)?.[1];
|
|
377
|
+
if (date) currentDate = date;
|
|
378
|
+
entries.push({ kind: 'note', title: `Ledger section: ${line.replace(/^##\s*/, '')} (imported)`, body: [], anchor: 'ledger', date: currentDate });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (heading && LEDGER_KINDS.includes(heading[1])) {
|
|
382
|
+
flush();
|
|
383
|
+
const rest = heading[2].trim();
|
|
384
|
+
const date = /(\d{4}-\d{2}-\d{2})/.exec(rest)?.[1] ?? currentDate;
|
|
385
|
+
current = { kind: heading[1], title: rest, body: [], anchor: null, date };
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (line === '---') { flush(); continue; }
|
|
389
|
+
if (!sawSection && !current) { banner.push(line); continue; }
|
|
390
|
+
if (current) current.body.push(line);
|
|
391
|
+
else if (line.trim()) entries.push({ kind: 'note', title: 'Ledger prose (imported)', body: [line], anchor: 'ledger', date: currentDate });
|
|
392
|
+
}
|
|
393
|
+
flush();
|
|
394
|
+
|
|
395
|
+
const validator = getJudgmentValidator();
|
|
396
|
+
const probeProvenance = { actor: 'agent', written_at: ts(DEFAULT_DATE), via: 'import' };
|
|
397
|
+
|
|
398
|
+
const events = [];
|
|
399
|
+
if (banner.filter(Boolean).length) {
|
|
400
|
+
events.push({
|
|
401
|
+
date: DEFAULT_DATE,
|
|
402
|
+
event: { kind: 'note', title: 'Ledger banner (imported)', body: banner.filter(Boolean).join('\n'), anchor: 'ledger-header' },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
const body = (entry.body ?? []).filter(Boolean).join('\n').trim();
|
|
407
|
+
const event = { kind: entry.kind, title: entry.title, body };
|
|
408
|
+
if (entry.anchor) event.anchor = entry.anchor;
|
|
409
|
+
if (entry.kind === 'decide' || entry.kind === 'kill') {
|
|
410
|
+
const rejectedLine = /\*\*Rejected[^:]*:\*\*\s*([\s\S]*?)(?:\n\*\*|$)/.exec(body)?.[1];
|
|
411
|
+
if (entry.kind === 'decide') {
|
|
412
|
+
event.rejected = rejectedLine
|
|
413
|
+
? [{ what: rejectedLine.replace(/\n/g, ' ').trim(), why: 'recorded in the imported entry body' }]
|
|
414
|
+
: [];
|
|
415
|
+
event.conviction = convictionFrom(/\*\*Conviction:\*\*([^\n]*)/.exec(body)?.[1] ?? '');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (entry.kind === 'override') {
|
|
419
|
+
event.reason = body.split('\n')[0] || entry.title;
|
|
420
|
+
}
|
|
421
|
+
if (!body) delete event.body;
|
|
422
|
+
|
|
423
|
+
const { valid } = validator.validate('ledger_event', { ...event, provenance: probeProvenance });
|
|
424
|
+
if (valid) {
|
|
425
|
+
events.push({ date: entry.date, event });
|
|
426
|
+
} else {
|
|
427
|
+
// pre-schema entry shape → honest note fallback, original heading kept
|
|
428
|
+
events.push({
|
|
429
|
+
date: entry.date,
|
|
430
|
+
event: { kind: 'note', title: `${entry.kind}: ${entry.title} (imported as note)`, body: body || entry.title, anchor: 'ledger' },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return events;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// The import
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
function readSources(cwd) {
|
|
442
|
+
const dir = join(cwd, 'docs', 'judgment');
|
|
443
|
+
const positionsDir = join(dir, 'positions');
|
|
444
|
+
const positionFiles = existsSync(positionsDir)
|
|
445
|
+
? readdirSync(positionsDir).filter((f) => f.endsWith('.md')).sort()
|
|
446
|
+
: [];
|
|
447
|
+
return {
|
|
448
|
+
register: readFileSync(join(dir, 'REGISTER.md'), 'utf8'),
|
|
449
|
+
ledger: readFileSync(join(dir, 'LEDGER.md'), 'utf8'),
|
|
450
|
+
objective: readFileSync(join(dir, 'OBJECTIVE.md'), 'utf8'),
|
|
451
|
+
positions: positionFiles.map((f) => ({
|
|
452
|
+
slug: f.replace(/\.md$/, ''),
|
|
453
|
+
rel: `docs/judgment/positions/${f}`,
|
|
454
|
+
text: readFileSync(join(positionsDir, f), 'utf8'),
|
|
455
|
+
})),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function buildPlan(sources) {
|
|
460
|
+
const objective = parseObjective(sources.objective);
|
|
461
|
+
const register = parseRegister(sources.register, objective.questions);
|
|
462
|
+
const ledger = parseLedger(sources.ledger);
|
|
463
|
+
|
|
464
|
+
const positions = [objective.position];
|
|
465
|
+
const notes = [objective.note];
|
|
466
|
+
for (const file of sources.positions) {
|
|
467
|
+
const parsed = parsePositionFile(file.slug, file.text, file.rel);
|
|
468
|
+
positions.push(parsed.position);
|
|
469
|
+
notes.push(parsed.note);
|
|
470
|
+
}
|
|
471
|
+
// Event order: ledger banner + entries (chronology), then register notes,
|
|
472
|
+
// then position prose notes.
|
|
473
|
+
const events = [...ledger, ...register.notes, ...notes];
|
|
474
|
+
return { positions, joints: register.joints, events };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function executePlan(cwd, plan) {
|
|
478
|
+
for (const p of plan.positions) {
|
|
479
|
+
await judgmentPositionCreate(cwd, p.record, { via: 'import', writtenAt: ts(p.date) });
|
|
480
|
+
}
|
|
481
|
+
for (const j of plan.joints) {
|
|
482
|
+
await judgmentJointAdd(cwd, j.record, { via: 'import', writtenAt: ts(j.date) });
|
|
483
|
+
}
|
|
484
|
+
for (const e of plan.events) {
|
|
485
|
+
await judgmentLedgerAppend(cwd, e.event, { via: 'import', writtenAt: ts(e.date) });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function diffProjections(stagingCwd, cwd) {
|
|
490
|
+
const stagingDir = join(stagingCwd, 'docs', 'judgment');
|
|
491
|
+
const files = ['REGISTER.md', 'LEDGER.md', 'OBJECTIVE.md', 'index.md',
|
|
492
|
+
...readdirSync(join(stagingDir, 'positions')).filter((f) => f.endsWith('.md')).map((f) => `positions/${f}`)];
|
|
493
|
+
return files.map((rel) => {
|
|
494
|
+
const generated = readFileSync(join(stagingDir, rel), 'utf8');
|
|
495
|
+
const currentPath = join(cwd, 'docs', 'judgment', rel);
|
|
496
|
+
const current = existsSync(currentPath) ? readFileSync(currentPath, 'utf8') : null;
|
|
497
|
+
return { file: `docs/judgment/${rel}`, changed: current !== generated, new: current === null };
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Run the import. Dry-run: stage in a temp workspace, report counts + the
|
|
503
|
+
* projection diff, write nothing into cwd. Real run: refuse if records
|
|
504
|
+
* exist, then write records through the writer (projections cut over as a
|
|
505
|
+
* side-effect of each write).
|
|
506
|
+
*
|
|
507
|
+
* @param {string} cwd
|
|
508
|
+
* @param {{ dryRun?: boolean }} [opts]
|
|
509
|
+
*/
|
|
510
|
+
export async function runJudgmentImport(cwd, { dryRun = false } = {}) {
|
|
511
|
+
// Preflight the configured canon provider: if judgment.provider selects an
|
|
512
|
+
// unimplemented/unknown backend this throws HERE, before any staging or
|
|
513
|
+
// copying — floor records must never land under a config that rejects them.
|
|
514
|
+
const { createJudgmentStore } = await import('../lib/judgment/store/index.js');
|
|
515
|
+
createJudgmentStore(cwd);
|
|
516
|
+
if (existsSync(join(cwd, 'docs', 'judgment', 'records'))) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
'judgment records already exist under docs/judgment/records/ — import is one-time (remove records/ to re-run; '
|
|
519
|
+
+ 'if a cutover was interrupted after records landed, regenerate projections via lib/judgment-gen.js#regenerateProjections)',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const sources = readSources(cwd);
|
|
523
|
+
const plan = buildPlan(sources);
|
|
524
|
+
|
|
525
|
+
// Stage the FULL import in a temp workspace: every record goes through the
|
|
526
|
+
// writer there, and the staged projections are what the human gate diffs
|
|
527
|
+
// against the current hand-written files (staging path is returned so the
|
|
528
|
+
// diff is actually inspectable).
|
|
529
|
+
const staging = mkdtempSync(join(tmpdir(), 'judgment-import-staging-'));
|
|
530
|
+
await executePlan(staging, plan);
|
|
531
|
+
const diff = diffProjections(staging, cwd);
|
|
532
|
+
|
|
533
|
+
if (!dryRun) {
|
|
534
|
+
// Crash-safe cutover: the writer-produced records land in cwd via copy →
|
|
535
|
+
// single rename (no partial records/ tree on interruption), then the
|
|
536
|
+
// projections regenerate from them. A crash after the rename self-heals:
|
|
537
|
+
// every subsequent writer op regenerates all projections.
|
|
538
|
+
const { cpSync, renameSync: rename } = await import('node:fs');
|
|
539
|
+
const { regenerateProjections } = await import('../lib/judgment-gen.js');
|
|
540
|
+
const dst = join(cwd, 'docs', 'judgment', 'records');
|
|
541
|
+
const tmpDst = `${dst}.importing.${process.pid}`;
|
|
542
|
+
cpSync(join(staging, 'docs', 'judgment', 'records'), tmpDst, { recursive: true });
|
|
543
|
+
rename(tmpDst, dst);
|
|
544
|
+
regenerateProjections(cwd);
|
|
545
|
+
// Trust-on-first-use baseline for the promoted TARGET records. Never use
|
|
546
|
+
// the staging manifest here: its workspace paths are disposable.
|
|
547
|
+
writeManifest(cwd, computeRecordHashes(cwd));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
dryRun,
|
|
552
|
+
staging,
|
|
553
|
+
counts: {
|
|
554
|
+
positions: plan.positions.length,
|
|
555
|
+
joints: plan.joints.length,
|
|
556
|
+
ledger_events: plan.events.length,
|
|
557
|
+
},
|
|
558
|
+
diff,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// CLI: node bin/judgment-import.js [--dry-run] [cwd]
|
|
563
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
564
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
565
|
+
const cwd = process.argv.slice(2).find((a) => !a.startsWith('--')) ?? process.cwd();
|
|
566
|
+
runJudgmentImport(cwd, { dryRun }).then((result) => {
|
|
567
|
+
console.log(JSON.stringify(result, null, 2));
|
|
568
|
+
}).catch((err) => {
|
|
569
|
+
console.error(err.message);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
"maximum": 4,
|
|
41
41
|
"description": "COMP-TRIAGE-5: raw machine tier (0-4) from the front estimator/refinement. Maps to `complexity` via tierToComplexity."
|
|
42
42
|
},
|
|
43
|
+
"triageConfidence": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["high", "medium", "low"],
|
|
46
|
+
"description": "COMP-TRIAGE-6: confidence from the front scope estimate."
|
|
47
|
+
},
|
|
43
48
|
"estimateSource": {
|
|
44
49
|
"type": "string",
|
|
45
50
|
"enum": ["front", "refined", "escalated"],
|