@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.
Files changed (138) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/bin/compose.js +288 -0
  3. package/bin/git-hooks/pre-push.template +29 -0
  4. package/bin/judgment-import.js +572 -0
  5. package/contracts/feature-json.schema.json +5 -0
  6. package/contracts/judgment-record.schema.json +821 -0
  7. package/dist/assets/App-PkZzHeMj.js +894 -0
  8. package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
  9. package/dist/assets/arc-BafGpyqE.js +1 -0
  10. package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
  11. package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
  12. package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
  13. package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
  14. package/dist/assets/channel-qVK_qn4E.js +1 -0
  15. package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
  16. package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
  17. package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
  19. package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
  20. package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
  21. package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
  22. package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
  25. package/dist/assets/clone-Pu3RyLUh.js +1 -0
  26. package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
  27. package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
  28. package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
  29. package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
  30. package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
  31. package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
  32. package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
  33. package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
  34. package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
  35. package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
  36. package/dist/assets/graph-Bi99_6Yf.js +331 -0
  37. package/dist/assets/graph-RE4I7Ty7.js +1 -0
  38. package/dist/assets/index-Rm2RE-c0.js +123 -0
  39. package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
  40. package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
  41. package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
  42. package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
  43. package/dist/assets/katex-DkKDou_j.js +257 -0
  44. package/dist/assets/layout-BSf33zm8.js +1 -0
  45. package/dist/assets/{linear-CsXqv70E.js → linear-AvSTWMqx.js} +1 -1
  46. package/dist/assets/min-QBM8H4xN.js +1 -0
  47. package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
  48. package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
  49. package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
  50. package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
  51. package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
  52. package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
  53. package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
  54. package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
  56. package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
  57. package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
  58. package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
  59. package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
  60. package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
  61. package/dist/index.html +2 -2
  62. package/lib/bug-escalation.js +30 -4
  63. package/lib/build.js +777 -52
  64. package/lib/canon-guard.js +223 -0
  65. package/lib/canon-registry.js +187 -0
  66. package/lib/codex-preflight.js +26 -4
  67. package/lib/dispatch-ledger.js +301 -0
  68. package/lib/dispatch-metrics.js +236 -0
  69. package/lib/experiment-judge.js +6 -1
  70. package/lib/feature-writer.js +9 -0
  71. package/lib/gsd.js +11 -2
  72. package/lib/hooks-status.js +32 -3
  73. package/lib/idempotency.js +33 -8
  74. package/lib/judgment/schema.js +23 -0
  75. package/lib/judgment/store/index.js +207 -0
  76. package/lib/judgment/store/records.js +405 -0
  77. package/lib/judgment-attest.js +259 -0
  78. package/lib/judgment-gen.js +691 -0
  79. package/lib/judgment-verify.js +153 -0
  80. package/lib/judgment-write-guard.js +230 -0
  81. package/lib/judgment-writer.js +3403 -0
  82. package/lib/lane-gate.js +2 -0
  83. package/lib/lifecycle-modes.js +42 -0
  84. package/lib/local-claude-connector.js +199 -54
  85. package/lib/mcp-enforcement.js +21 -35
  86. package/lib/result-normalizer.js +93 -15
  87. package/lib/review-normalize.js +4 -0
  88. package/lib/stratum-mcp-client.js +131 -6
  89. package/package.json +2 -2
  90. package/server/compose-mcp-tools.js +49 -1
  91. package/server/compose-mcp.js +220 -0
  92. package/server/mcp-tool-policy.js +1 -0
  93. package/dist/assets/App-BG92y_CY.js +0 -896
  94. package/dist/assets/abnfDiagram-VRR7QNED-Bip6cMjP.js +0 -1
  95. package/dist/assets/arc-CyMTM07c.js +0 -1
  96. package/dist/assets/architectureDiagram-ZJ3FMSHR-BukV0CJw.js +0 -36
  97. package/dist/assets/blockDiagram-677ZJIJ3-Bjb_MNB4.js +0 -132
  98. package/dist/assets/channel-BzkmtaLy.js +0 -1
  99. package/dist/assets/chunk-EX3LRPZG-G_MZEji_.js +0 -231
  100. package/dist/assets/chunk-MOJQB5TN-CQZQYMcz.js +0 -88
  101. package/dist/assets/chunk-RYQCIY6F-CmAda4sZ.js +0 -1
  102. package/dist/assets/chunk-V7JOEXUC-CbjBz-Ek.js +0 -206
  103. package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
  104. package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
  105. package/dist/assets/cynefin-VYW2F7L2-C3n1PNJC.js +0 -178
  106. package/dist/assets/cynefinDiagram-TSTJHNR4-C8N_CvKJ.js +0 -62
  107. package/dist/assets/dagre-VKFMJZFB-B2pkwDhN.js +0 -4
  108. package/dist/assets/diagram-FQU43EPY-DPHD_t-1.js +0 -3
  109. package/dist/assets/diagram-G47NLZAW-DGmHIZRr.js +0 -24
  110. package/dist/assets/diagram-NH7WQ7WH-BlLWwKVt.js +0 -24
  111. package/dist/assets/diagram-OA4YK3LP-DEv704Re.js +0 -30
  112. package/dist/assets/diagram-WEI45ONY-Bj6PGUCe.js +0 -41
  113. package/dist/assets/ebnfDiagram-CCIWWBDH-Ch-icksT.js +0 -1
  114. package/dist/assets/erDiagram-Q63AITRT-CI3lwEFA.js +0 -85
  115. package/dist/assets/flowDiagram-23GEKE2U-DcUANpAz.js +0 -156
  116. package/dist/assets/ganttDiagram-NO4QXBWP-Cy0Ie8KS.js +0 -292
  117. package/dist/assets/gitGraphDiagram-IHSO6WYX-BswI52az.js +0 -106
  118. package/dist/assets/graph-BXPQrYYB.js +0 -331
  119. package/dist/assets/graph-C9eacEi8.js +0 -1
  120. package/dist/assets/index-B1H7-G2y.js +0 -119
  121. package/dist/assets/infoDiagram-FWYZ7A6U-BBunqvr-.js +0 -2
  122. package/dist/assets/katex-C5jXJg4s.js +0 -257
  123. package/dist/assets/layout-DEXfKzaS.js +0 -1
  124. package/dist/assets/map-Czzmt4hB.js +0 -1
  125. package/dist/assets/pegDiagram-2B236MQR-HpQyrDcm.js +0 -1
  126. package/dist/assets/pieDiagram-ENE6RG2P-xkcpHwnY.js +0 -39
  127. package/dist/assets/quadrantDiagram-ABIIQ3AL-kebZIOmu.js +0 -7
  128. package/dist/assets/railroadDiagram-RFXS5EU6-DNvGwWm9.js +0 -1
  129. package/dist/assets/sankeyDiagram-HTMAVEWB-DOEEcn52.js +0 -40
  130. package/dist/assets/sequenceDiagram-DBY2YBRQ-DynIVCk_.js +0 -162
  131. package/dist/assets/sizeCapture-X5ZJPWSS-DJXFjUUL.js +0 -1
  132. package/dist/assets/stateDiagram-2N3HPSRC-OZ7trFh5.js +0 -1
  133. package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
  134. package/dist/assets/swimlanes-5IMT3BWC-DULYAFmB.js +0 -2
  135. package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
  136. package/dist/assets/vennDiagram-L72KCM5P-CdKmtcFL.js +0 -34
  137. package/dist/assets/wardleyDiagram-EHGQE667-DknGwjCA.js +0 -78
  138. package/dist/assets/xychartDiagram-FW5EYKEG-DdYyKfQ3.js +0 -7
@@ -0,0 +1,691 @@
1
+ /**
2
+ * lib/judgment-gen.js — judgment projections (S04).
3
+ *
4
+ * records → REGISTER.md, LEDGER.md, OBJECTIVE.md, SITUATION.md,
5
+ * people/<slug>.md, positions/<slug>.md, and the OKF bundle root index.md.
6
+ * Projections are PURE OUTPUT: generation
7
+ * reads only records — never an existing projection — so any hand-edit is
8
+ * overwritten by the next regeneration and there are no preserved sections
9
+ * (curated prose is `note` records rendered at their anchors).
10
+ *
11
+ * Output is a deterministic function of the records (no wall-clock in the
12
+ * bytes), which is what makes the fixed-point roundtrip guard meaningful:
13
+ * regen of the generator's own output is byte-identical.
14
+ *
15
+ * OKF (design Decision 8): per-item files carry frontmatter type/title/
16
+ * timestamp plus the smartmemory { reference: true, origin } extension;
17
+ * `resource` is emitted ONLY when a record carries a real provider id AND
18
+ * an enrichment workspace (team_id) is configured — a made-up resource
19
+ * would be a dangling identity claim. The bundle root carries
20
+ * okf_version "0.1" and no type (okf.ts renderOkf rule).
21
+ */
22
+ import {
23
+ readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, readdirSync,
24
+ } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import {
27
+ createJudgmentStore,
28
+ effectiveStore,
29
+ goalCutoverComplete,
30
+ } from './judgment/store/index.js';
31
+
32
+ function atomicWrite(path, content) {
33
+ const tmp = `${path}.tmp.${process.pid}`;
34
+ try {
35
+ writeFileSync(tmp, content);
36
+ renameSync(tmp, path);
37
+ } catch (err) {
38
+ try { unlinkSync(tmp); } catch { /* tmp may not exist */ }
39
+ throw err;
40
+ }
41
+ }
42
+
43
+ function smartmemoryTeamId(cwd) {
44
+ try {
45
+ const config = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf8'));
46
+ return config?.judgment?.enrichment?.smartmemory?.team_id ?? null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // ── snapshot ────────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Load everything projection-relevant from the store into a plain snapshot,
56
+ * so the generator core stays pure.
57
+ */
58
+ export function loadSnapshot(cwd) {
59
+ const rawStore = createJudgmentStore(cwd);
60
+ const store = effectiveStore(rawStore);
61
+ const positions = store.listPositionSlugs().map((slug) => ({
62
+ slug,
63
+ chain: store.readPositionChain(slug),
64
+ status: store.derivePositionStatus(slug),
65
+ }));
66
+ return {
67
+ positions,
68
+ joints: store.listJoints(),
69
+ ledger: store.readLedgerEvents(),
70
+ people: store.listPeople(),
71
+ situationEntities: store.listSituationEntities(),
72
+ goalChain: store.readGoalChain(),
73
+ goalState: store.readGoalState(),
74
+ goalCutoverComplete: goalCutoverComplete(store),
75
+ teamId: smartmemoryTeamId(cwd),
76
+ };
77
+ }
78
+
79
+ // ── rendering helpers ───────────────────────────────────────────────────────
80
+
81
+ function anchoredNotes(ledger, anchor) {
82
+ return ledger.filter((e) => e.kind === 'note' && e.anchor === anchor);
83
+ }
84
+
85
+ function renderNotes(notes) {
86
+ return notes.map((n) => `> **${n.title}** — ${n.body ?? ''}`.trimEnd()).join('\n\n');
87
+ }
88
+
89
+ // Plain YAML scalars that need no quoting: no leading/trailing space, none
90
+ // of the indicator/structure characters that would change the parse. Risky
91
+ // titles (e.g. "Jane: CEO") are emitted JSON-quoted, which is valid YAML
92
+ // double-quote style.
93
+ const YAML_PLAIN_SAFE_RE = /^[A-Za-z0-9](?:[A-Za-z0-9 _./()-]*[A-Za-z0-9_./()-])?$/;
94
+
95
+ function yamlScalar(value) {
96
+ const s = String(value);
97
+ return YAML_PLAIN_SAFE_RE.test(s) ? s : JSON.stringify(s);
98
+ }
99
+
100
+ function okfFrontmatter({ type, title, timestamp, resource }) {
101
+ const lines = ['---'];
102
+ if (type) lines.push(`type: ${type}`);
103
+ else lines.push('okf_version: "0.1"');
104
+ lines.push(`title: ${yamlScalar(title)}`);
105
+ if (timestamp) lines.push(`timestamp: "${timestamp}"`);
106
+ if (resource) lines.push(`resource: "${resource}"`);
107
+ lines.push('smartmemory:');
108
+ lines.push(' reference: true');
109
+ lines.push(' origin: compose-projection');
110
+ lines.push('---');
111
+ return lines.join('\n');
112
+ }
113
+
114
+ function buildResource(teamId, providerId) {
115
+ if (!teamId || !providerId) return null;
116
+ return `smartmemory://${encodeURIComponent(teamId)}/${encodeURIComponent(providerId)}`;
117
+ }
118
+
119
+ function auditValue(value) {
120
+ const encoded = JSON.stringify(value);
121
+ return encoded === undefined ? String(value) : encoded;
122
+ }
123
+
124
+ function renderTraceLines(trace = [], { prefix = ' - ' } = {}) {
125
+ return trace.map((entry) => {
126
+ const prior = Object.entries(entry.prior ?? {})
127
+ .map(([key, value]) => `${key}=${auditValue(value)}`)
128
+ .join(', ');
129
+ return `${prefix}corrected from ${prior} at ${entry.corrected_at ?? 'unknown'}`;
130
+ });
131
+ }
132
+
133
+ function renderRecordedAndRemoval(entry, { prefix = ' - ' } = {}) {
134
+ const lines = [];
135
+ lines.push(`${prefix}recorded at ${entry.provenance?.written_at ?? 'unknown'}`);
136
+ if (entry.removed) {
137
+ lines.push(`${prefix}removed at ${entry.removed.at ?? 'unknown'} — ${entry.removed.reason}`);
138
+ }
139
+ return lines;
140
+ }
141
+
142
+ function renderAuditedFact(fact) {
143
+ const lines = [`- **${fact.id}** ${fact.text}`];
144
+ lines.push(` - channel: \`${fact.channel}\``);
145
+ if (fact.via !== undefined) lines.push(` - via: ${fact.via}`);
146
+ lines.push(` - at: ${fact.at}`);
147
+ lines.push(...renderTraceLines(fact.trace));
148
+ return lines.join('\n');
149
+ }
150
+
151
+ const PERSON_SECTIONS = ['role', 'life', 'stated', 'revealed'];
152
+
153
+ function sectionTitle(section) {
154
+ return section[0].toUpperCase() + section.slice(1);
155
+ }
156
+
157
+ function orderedPairLabel(left, right) {
158
+ const suffix = (id) => Number.parseInt(String(id).replace(/^[^0-9]+/, ''), 10);
159
+ const ids = [left, right].sort((a, b) => suffix(a) - suffix(b) || a.localeCompare(b));
160
+ return `${ids[0]} ↔ ${ids[1]}`;
161
+ }
162
+
163
+ function renderPerson(person) {
164
+ const lifecycle = person.facts.some((fact) => fact.channel === 'said') ? 'spoken' : 'stub';
165
+ const parts = [okfFrontmatter({
166
+ type: 'person',
167
+ title: person.display_name,
168
+ timestamp: person.provenance?.written_at,
169
+ })];
170
+ parts.push('');
171
+ parts.push(`# ${person.display_name}`);
172
+ parts.push('');
173
+ parts.push(`**Lifecycle:** ${lifecycle}`);
174
+ parts.push(`**Record:** \`${person.slug}\``);
175
+
176
+ const factsById = new Map(person.facts.map((fact) => [fact.id, fact]));
177
+ const rendered = new Set();
178
+ for (const section of PERSON_SECTIONS) {
179
+ parts.push('');
180
+ parts.push(`## ${sectionTitle(section)}`);
181
+ parts.push('');
182
+ const sectionLines = [];
183
+ for (const fact of person.facts.filter((entry) => entry.section === section)) {
184
+ if (rendered.has(fact.id)) continue;
185
+ const paired = fact.diverges_with ? factsById.get(fact.diverges_with) : null;
186
+ const isStatedRevealedPair = (
187
+ section === 'stated'
188
+ && paired?.section === 'revealed'
189
+ && paired.diverges_with === fact.id
190
+ );
191
+ sectionLines.push(renderAuditedFact(fact));
192
+ rendered.add(fact.id);
193
+ if (isStatedRevealedPair) {
194
+ sectionLines.push(renderAuditedFact(paired));
195
+ sectionLines.push(` - divergence pair: ${orderedPairLabel(fact.id, paired.id)}`);
196
+ rendered.add(paired.id);
197
+ }
198
+ }
199
+ parts.push(sectionLines.length > 0 ? sectionLines.join('\n') : 'No facts recorded.');
200
+ }
201
+
202
+ parts.push('');
203
+ parts.push('## Edges');
204
+ parts.push('');
205
+ if (person.edges.length === 0) {
206
+ parts.push('None.');
207
+ } else {
208
+ for (const edge of person.edges) {
209
+ parts.push(`- **${edge.id}** \`${edge.removed ? 'removed' : 'active'}\` — ${edge.kind} → [${edge.to}](${edge.to}.md)`);
210
+ parts.push(...renderRecordedAndRemoval(edge));
211
+ }
212
+ }
213
+
214
+ parts.push('');
215
+ parts.push('## Open fields');
216
+ parts.push('');
217
+ if (person.open_fields.length === 0) {
218
+ parts.push('None.');
219
+ } else {
220
+ for (const field of person.open_fields) {
221
+ parts.push(`- **${field.id}** \`${field.status}\` — ${field.name}`);
222
+ parts.push(` - filled_by: ${field.filled_by ?? '—'}`);
223
+ parts.push(` - recorded at ${field.provenance?.written_at ?? 'unknown'}`);
224
+ parts.push(...renderTraceLines(field.trace));
225
+ }
226
+ }
227
+
228
+ parts.push('');
229
+ parts.push('## Load links');
230
+ parts.push('');
231
+ if (person.load_links.length === 0) {
232
+ parts.push('None.');
233
+ } else {
234
+ for (const link of person.load_links) {
235
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
236
+ parts.push(...renderRecordedAndRemoval(link));
237
+ }
238
+ }
239
+ return parts.join('\n') + '\n';
240
+ }
241
+
242
+ function latestWrittenAt(records) {
243
+ return records
244
+ .map((record) => record.provenance?.written_at)
245
+ .filter(Boolean)
246
+ .sort()
247
+ .at(-1);
248
+ }
249
+
250
+ function renderSituation(snapshot) {
251
+ const entities = [...(snapshot.situationEntities ?? [])].sort((a, b) => (
252
+ a.display_name.localeCompare(b.display_name) || a.slug.localeCompare(b.slug)
253
+ ));
254
+ const parts = [okfFrontmatter({
255
+ type: 'situation',
256
+ title: 'Situation',
257
+ timestamp: latestWrittenAt(entities),
258
+ })];
259
+ parts.push('');
260
+ parts.push('# Situation');
261
+ if (entities.length === 0) {
262
+ parts.push('');
263
+ parts.push('No situation entities recorded.');
264
+ }
265
+ for (const entity of entities) {
266
+ parts.push('');
267
+ parts.push(`## ${entity.display_name} (\`${entity.slug}\`)`);
268
+ parts.push('');
269
+ parts.push('### Facts');
270
+ parts.push('');
271
+ if (entity.facts.length === 0) {
272
+ parts.push('None.');
273
+ } else {
274
+ for (const fact of entity.facts) parts.push(renderAuditedFact(fact));
275
+ }
276
+
277
+ parts.push('');
278
+ parts.push('### Owed');
279
+ parts.push('');
280
+ if (entity.owed.length === 0) {
281
+ parts.push('None.');
282
+ } else {
283
+ for (const owed of entity.owed) {
284
+ parts.push(`> **OWED ${owed.id} · ${owed.status}** ${owed.name}`);
285
+ parts.push(`> - why load-bearing: ${owed.why_load_bearing}`);
286
+ parts.push(`> - filled_by: ${owed.filled_by ?? '—'}`);
287
+ parts.push(`> - recorded at ${owed.provenance?.written_at ?? 'unknown'}`);
288
+ parts.push(...renderTraceLines(owed.trace, { prefix: '> - ' }));
289
+ }
290
+ }
291
+
292
+ parts.push('');
293
+ parts.push('### Load links');
294
+ parts.push('');
295
+ if (entity.load_links.length === 0) {
296
+ parts.push('None.');
297
+ } else {
298
+ for (const link of entity.load_links) {
299
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — fact ${link.fact} carries ${link.carries}`);
300
+ parts.push(...renderRecordedAndRemoval(link));
301
+ }
302
+ }
303
+ }
304
+ return parts.join('\n') + '\n';
305
+ }
306
+
307
+ function renderClaim(claim) {
308
+ const lines = [`- **${claim.id}** \`[${claim.grounding}]\`${claim.owner_locked ? ' `[owner-locked]`' : ''} ${claim.text}`];
309
+ if (Array.isArray(claim.supports) && claim.supports.length > 0) {
310
+ lines.push(` - supports: ${claim.supports.join(', ')}`);
311
+ }
312
+ if (claim.elicitation) {
313
+ lines.push(` - elicitation: asked "${claim.elicitation.asked}" (answered ${claim.elicitation.answered_at}, ref ${claim.elicitation.answer_ref})`);
314
+ }
315
+ return lines.join('\n');
316
+ }
317
+
318
+ function renderPosition(position, snapshot) {
319
+ const { slug, chain, status } = position;
320
+ const current = chain[chain.length - 1];
321
+ const resource = buildResource(snapshot.teamId, current?.provider_ids?.smartmemory);
322
+ const parts = [okfFrontmatter({
323
+ type: 'position',
324
+ title: slug,
325
+ timestamp: current?.provenance?.written_at,
326
+ resource,
327
+ })];
328
+ parts.push('');
329
+ parts.push(`# ${slug}`);
330
+ parts.push('');
331
+ parts.push(`**Status:** ${status} · **Conviction:** ${current.conviction?.level ?? '—'} (${current.conviction?.source ?? '—'})`);
332
+ if (current.retracted) {
333
+ parts.push('');
334
+ parts.push('**Retracted.** The latest revision is a tombstone.');
335
+ }
336
+ if (current.claims?.length) {
337
+ parts.push('');
338
+ parts.push(`## Claims (r${current.rev})`);
339
+ parts.push('');
340
+ for (const claim of current.claims) parts.push(renderClaim(claim));
341
+ }
342
+ if (current.rejected_alternatives?.length) {
343
+ parts.push('');
344
+ parts.push('## Rejected alternatives');
345
+ parts.push('');
346
+ for (const alt of current.rejected_alternatives) parts.push(`- ${alt.what} — ${alt.why}`);
347
+ }
348
+ parts.push('');
349
+ parts.push('## History');
350
+ parts.push('');
351
+ for (const rev of chain) {
352
+ const marks = [];
353
+ if (rev.supersedes) marks.push(`supersedes ${rev.supersedes}`);
354
+ if (rev.retracted) marks.push('tombstone');
355
+ parts.push(`- r${rev.rev} — ${rev.provenance?.written_at ?? 'unknown'}${marks.length ? ` (${marks.join(', ')})` : ''}`);
356
+ }
357
+ const notes = anchoredNotes(snapshot.ledger, `position:${slug}`);
358
+ if (notes.length) {
359
+ parts.push('');
360
+ parts.push(renderNotes(notes));
361
+ }
362
+ return parts.join('\n') + '\n';
363
+ }
364
+
365
+ function renderJointSection(joint, snapshot) {
366
+ const parts = [`## ${joint.slug}`];
367
+ parts.push('');
368
+ parts.push(`- **Question:** ${joint.question}`);
369
+ parts.push(`- **If true:** ${joint.branch_true}`);
370
+ parts.push(`- **If false:** ${joint.branch_false}`);
371
+ parts.push(`- **Method:** ${joint.resolve_by} · cost ${joint.cost} · rank ${joint.rank}`);
372
+ parts.push(`- **State:** ${joint.state}`);
373
+ if (joint.flags?.length) parts.push(`- **Flags:** ${joint.flags.join(', ')}`);
374
+ if (joint.ext) {
375
+ parts.push(joint.ext.judgment_dispatch
376
+ ? `- **Ext:** judgment-dispatch — ${joint.ext.reason}`
377
+ : `- **Ext:** sharpened "${joint.ext.sharpened_question}" · bar: ${joint.ext.bar} · falsifier: ${joint.ext.falsifier}`);
378
+ }
379
+ if (joint.straddle) {
380
+ parts.push(`- **Straddle:** signal ${joint.straddle.discriminating_signal} · kill: ${joint.straddle.kill_criteria}`);
381
+ }
382
+ if (joint.resolution) {
383
+ const r = joint.resolution;
384
+ const detail = r.evidence ?? r.learned ?? r.reason ?? r.why ?? '';
385
+ parts.push(`- **Resolution:** ${r.outcome}${detail ? ` — ${typeof detail === 'string' ? detail : JSON.stringify(detail)}` : ''}`);
386
+ if (r.outcome === 'inconclusive' && r.would_have_settled) {
387
+ parts.push(` - would have settled: ${r.would_have_settled}`);
388
+ }
389
+ if (r.ext_result) {
390
+ parts.push(` - ext result: ${r.ext_result.outcome} (${r.ext_result.found_or_provoked}) — ${r.ext_result.search_record}`);
391
+ }
392
+ }
393
+ if (joint.dissolution) {
394
+ parts.push(`- **Dissolved into:** ${joint.dissolution.decomposed_into.join(', ')}`);
395
+ }
396
+ const notes = anchoredNotes(snapshot.ledger, `joint:${joint.slug}`);
397
+ if (notes.length) {
398
+ parts.push('');
399
+ parts.push(renderNotes(notes));
400
+ }
401
+ return parts.join('\n');
402
+ }
403
+
404
+ const RANK_ORDER = { high: 0, medium: 1 };
405
+
406
+ function renderRegister(snapshot) {
407
+ const parts = ['# Judgment Register'];
408
+ const headerNotes = anchoredNotes(snapshot.ledger, 'register-header');
409
+ if (headerNotes.length) {
410
+ parts.push('');
411
+ parts.push(renderNotes(headerNotes));
412
+ }
413
+ const joints = [...snapshot.joints].sort((a, b) =>
414
+ (RANK_ORDER[a.rank] - RANK_ORDER[b.rank]) || a.slug.localeCompare(b.slug));
415
+ for (const joint of joints) {
416
+ parts.push('');
417
+ parts.push(renderJointSection(joint, snapshot));
418
+ }
419
+ const footerNotes = anchoredNotes(snapshot.ledger, 'register-footer');
420
+ if (footerNotes.length) {
421
+ parts.push('');
422
+ parts.push(renderNotes(footerNotes));
423
+ }
424
+ return parts.join('\n') + '\n';
425
+ }
426
+
427
+ const EVENT_DETAIL_KEYS = [
428
+ 'refs', 'rejected', 'conviction', 'trigger', 'open_joints', 'prediction',
429
+ 'disposition', 'recall_verdict', 'attribution', 'prediction_ref',
430
+ 'prediction_grade', 'reason', 'rank_change', 'elicitation',
431
+ 'intent_id', 'tool', 'op', 'rests_on',
432
+ ];
433
+
434
+ function renderLedger(snapshot) {
435
+ const parts = ['# Judgment Ledger'];
436
+ const headerNotes = anchoredNotes(snapshot.ledger, 'ledger-header');
437
+ if (headerNotes.length) {
438
+ parts.push('');
439
+ parts.push(renderNotes(headerNotes));
440
+ }
441
+ snapshot.ledger.forEach((event, i) => {
442
+ parts.push('');
443
+ parts.push(`## ${i + 1}. ${event.kind}: ${event.title}`);
444
+ if (event.body) {
445
+ parts.push('');
446
+ parts.push(event.body);
447
+ }
448
+ const details = EVENT_DETAIL_KEYS
449
+ .filter((k) => event[k] !== undefined && !(Array.isArray(event[k]) && event[k].length === 0))
450
+ .map((k) => `- ${k}: ${typeof event[k] === 'string' ? event[k] : JSON.stringify(event[k])}`);
451
+ if (event.anchor) details.push(`- anchor: ${event.anchor}`);
452
+ if (details.length) {
453
+ parts.push('');
454
+ parts.push(details.join('\n'));
455
+ }
456
+ parts.push('');
457
+ parts.push(`*${event.provenance?.written_at ?? ''}${event.provenance?.via ? ` · via ${event.provenance.via}` : ''}*`);
458
+ });
459
+ return parts.join('\n') + '\n';
460
+ }
461
+
462
+ function renderGoalClause(clause) {
463
+ const lines = [`- **${clause.id}** ${clause.text}`];
464
+ lines.push(` - channel: \`${clause.channel}\``);
465
+ if (clause.via !== undefined) lines.push(` - via: ${clause.via}`);
466
+ const citation = clause.elicitation;
467
+ lines.push(
468
+ ` - elicitation: asked "${citation.asked}" (answered ${citation.answered_at}, ref ${citation.answer_ref})`,
469
+ );
470
+ lines.push(...renderTraceLines(clause.trace));
471
+ return lines.join('\n');
472
+ }
473
+
474
+ function tableCell(value) {
475
+ return String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
476
+ }
477
+
478
+ function renderGoalObjective(snapshot) {
479
+ const chain = snapshot.goalChain ?? [];
480
+ const current = chain.at(-1);
481
+ if (!current) {
482
+ return [
483
+ okfFrontmatter({ type: 'goal', title: 'Objective' }),
484
+ '',
485
+ '# Objective',
486
+ '',
487
+ 'No goal recorded.',
488
+ ].join('\n') + '\n';
489
+ }
490
+
491
+ const state = snapshot.goalState ?? { joints: [], load_links: [] };
492
+ const parts = [okfFrontmatter({
493
+ type: 'goal',
494
+ title: 'Objective',
495
+ timestamp: current.provenance?.written_at,
496
+ })];
497
+ parts.push('');
498
+ parts.push('# Objective');
499
+ parts.push('');
500
+ parts.push(`**Current version:** v${current.version}`);
501
+ const draft = (
502
+ ['import', 'migration'].includes(current.provenance?.via)
503
+ && !current.ratification
504
+ );
505
+ if (draft) {
506
+ parts.push('');
507
+ parts.push('**DRAFT HEALTH WARNING:** This imported/migrated goal is not owner-ratified.');
508
+ }
509
+
510
+ parts.push('');
511
+ parts.push(`## Current clauses (v${current.version})`);
512
+ parts.push('');
513
+ for (const clause of current.clauses) parts.push(renderGoalClause(clause));
514
+
515
+ parts.push('');
516
+ parts.push('## Ratification citation');
517
+ parts.push('');
518
+ if (current.ratification) {
519
+ parts.push(`- asked "${current.ratification.asked}"`);
520
+ parts.push(`- quote: "${current.ratification.quote}"`);
521
+ parts.push(
522
+ `- answered ${current.ratification.answered_at}, ref ${current.ratification.answer_ref}`,
523
+ );
524
+ } else {
525
+ parts.push('None — imported/migrated draft.');
526
+ }
527
+
528
+ parts.push('');
529
+ parts.push('## Goal joints');
530
+ parts.push('');
531
+ if ((state.joints ?? []).length === 0) {
532
+ parts.push('None.');
533
+ } else {
534
+ for (const link of state.joints) {
535
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — [${link.joint}](REGISTER.md#${link.joint})`);
536
+ parts.push(...renderRecordedAndRemoval(link));
537
+ }
538
+ }
539
+
540
+ parts.push('');
541
+ parts.push('## Load-link bill');
542
+ parts.push('');
543
+ if ((state.load_links ?? []).length === 0) {
544
+ parts.push('None.');
545
+ } else {
546
+ for (const link of state.load_links) {
547
+ const version = Number(/^v([1-9][0-9]*)#/.exec(link.clause)?.[1]);
548
+ const versionMark = version === current.version ? 'current version' : 'superseded version';
549
+ parts.push(`- **${link.id}** \`${link.removed ? 'removed' : 'active'}\` — ${link.clause} \`${versionMark}\` carries ${link.carries}`);
550
+ parts.push(...renderRecordedAndRemoval(link));
551
+ }
552
+ }
553
+
554
+ parts.push('');
555
+ parts.push('## Trajectory');
556
+ parts.push('');
557
+ parts.push('| Version | Date | Provocation | Diff note | Wording fix |');
558
+ parts.push('|---|---|---|---|---|');
559
+ for (const version of chain) {
560
+ const provocation = version.provocation?.quote ?? 'unknown (migrated)';
561
+ const wordingFix = version.clauses.some((clause) => (clause.trace?.length ?? 0) > 0)
562
+ ? 'yes'
563
+ : 'no';
564
+ parts.push(
565
+ `| v${version.version} | ${tableCell(version.provenance?.written_at ?? 'unknown')} | ${tableCell(provocation)} | ${tableCell(version.diff_note)} | ${wordingFix} |`,
566
+ );
567
+ }
568
+ return parts.join('\n') + '\n';
569
+ }
570
+
571
+ function renderObjective(snapshot) {
572
+ if (snapshot.goalCutoverComplete) return renderGoalObjective(snapshot);
573
+ const objective = (snapshot.positions ?? []).find((p) => p.slug === 'objective');
574
+ if (objective) return renderPosition(objective, snapshot);
575
+ return [
576
+ okfFrontmatter({ type: 'position', title: 'objective' }),
577
+ '',
578
+ '# objective',
579
+ '',
580
+ 'No objective recorded.',
581
+ ].join('\n') + '\n';
582
+ }
583
+
584
+ function renderIndex(snapshot) {
585
+ const positions = (snapshot.positions ?? []).filter(
586
+ (position) => !(snapshot.goalCutoverComplete && position.slug === 'objective'),
587
+ );
588
+ const parts = [okfFrontmatter({ type: null, title: 'Judgment canon' })];
589
+ parts.push('');
590
+ parts.push('# Judgment canon (generated)');
591
+ parts.push('');
592
+ parts.push('All files in this bundle are projections of `records/` — regenerated, never hand-edited.');
593
+ parts.push('');
594
+ parts.push('- [Register](REGISTER.md)');
595
+ parts.push('- [Ledger](LEDGER.md)');
596
+ parts.push('- [Objective](OBJECTIVE.md)');
597
+ if (positions.length) {
598
+ parts.push('');
599
+ parts.push('## Positions');
600
+ parts.push('');
601
+ for (const p of positions) {
602
+ parts.push(`- [${p.slug}](positions/${p.slug}.md) — ${p.status}`);
603
+ }
604
+ }
605
+ return parts.join('\n') + '\n';
606
+ }
607
+
608
+ // ── public surface ──────────────────────────────────────────────────────────
609
+
610
+ /**
611
+ * Pure generator core: snapshot → { relPath: content }. Exported for tests;
612
+ * `now` is accepted for interface stability but the output deliberately
613
+ * contains no wall-clock bytes (fixed-point requirement).
614
+ */
615
+ export function generateFromRecords(snapshot, { now } = {}) { // eslint-disable-line no-unused-vars
616
+ const files = {
617
+ 'docs/judgment/REGISTER.md': renderRegister(snapshot),
618
+ 'docs/judgment/LEDGER.md': renderLedger(snapshot),
619
+ 'docs/judgment/OBJECTIVE.md': renderObjective(snapshot),
620
+ 'docs/judgment/SITUATION.md': renderSituation(snapshot),
621
+ 'docs/judgment/index.md': renderIndex(snapshot),
622
+ };
623
+ for (const person of [...(snapshot.people ?? [])].sort((a, b) => a.slug.localeCompare(b.slug))) {
624
+ files[`docs/judgment/people/${person.slug}.md`] = renderPerson(person);
625
+ }
626
+ for (const position of (snapshot.positions ?? [])) {
627
+ if (snapshot.goalCutoverComplete && position.slug === 'objective') continue;
628
+ files[`docs/judgment/positions/${position.slug}.md`] = renderPosition(position, snapshot);
629
+ }
630
+ return files;
631
+ }
632
+
633
+ const MANAGED_MARKDOWN_DIRS = [
634
+ 'docs/judgment/people',
635
+ 'docs/judgment/positions',
636
+ ];
637
+
638
+ function managedStaleProjections(cwd, files) {
639
+ const stale = [];
640
+ for (const relDir of MANAGED_MARKDOWN_DIRS) {
641
+ const dir = join(cwd, relDir);
642
+ if (!existsSync(dir)) continue;
643
+ const entries = readdirSync(dir, { withFileTypes: true })
644
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
645
+ .sort((a, b) => a.name.localeCompare(b.name));
646
+ for (const entry of entries) {
647
+ const rel = `${relDir}/${entry.name}`;
648
+ if (!(rel in files)) stale.push(rel);
649
+ }
650
+ }
651
+ return stale;
652
+ }
653
+
654
+ /**
655
+ * Regenerate every projection from records on disk. Atomic per file.
656
+ * @returns {{ files: string[] }} relative paths written
657
+ */
658
+ export function regenerateProjections(cwd) {
659
+ const files = generateFromRecords(loadSnapshot(cwd));
660
+ for (const [rel, content] of Object.entries(files)) {
661
+ const path = join(cwd, rel);
662
+ mkdirSync(join(path, '..'), { recursive: true });
663
+ atomicWrite(path, content);
664
+ }
665
+ for (const rel of managedStaleProjections(cwd, files)) {
666
+ unlinkSync(join(cwd, rel));
667
+ }
668
+ return { files: Object.keys(files) };
669
+ }
670
+
671
+ /**
672
+ * Fixed-point roundtrip guard: regenerate in memory and compare byte-for-byte
673
+ * against what is on disk. Any drift (hand-edit, stale projection) fails.
674
+ * @returns {{ fixedPoint: boolean, diffs: string[] }}
675
+ */
676
+ export function checkProjectionRoundtrip(cwd) {
677
+ const files = generateFromRecords(loadSnapshot(cwd));
678
+ const diffs = [];
679
+ for (const [rel, content] of Object.entries(files)) {
680
+ const path = join(cwd, rel);
681
+ if (!existsSync(path)) {
682
+ diffs.push(`${rel} (missing)`);
683
+ continue;
684
+ }
685
+ if (readFileSync(path, 'utf8') !== content) diffs.push(`${rel} (drift)`);
686
+ }
687
+ for (const rel of managedStaleProjections(cwd, files)) {
688
+ diffs.push(`${rel} (orphan — not emitted by the generator)`);
689
+ }
690
+ return { fixedPoint: diffs.length === 0, diffs };
691
+ }