@smartmemory/compose 0.3.5-beta → 0.3.6-beta

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 (76) hide show
  1. package/bin/judgment-import.js +565 -0
  2. package/contracts/judgment-record.schema.json +400 -0
  3. package/dist/assets/{App-BG92y_CY.js → App-BG3ngu8H.js} +92 -92
  4. package/dist/assets/{abnfDiagram-VRR7QNED-Bip6cMjP.js → abnfDiagram-VRR7QNED-CjB_sD3D.js} +1 -1
  5. package/dist/assets/{arc-CyMTM07c.js → arc-_v4hR_uD.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BukV0CJw.js → architectureDiagram-ZJ3FMSHR-DreJmzXQ.js} +1 -1
  7. package/dist/assets/{blockDiagram-677ZJIJ3-Bjb_MNB4.js → blockDiagram-677ZJIJ3-BG9-c0O1.js} +1 -1
  8. package/dist/assets/{c4Diagram-LMCZKHZV-CUmTCrMW.js → c4Diagram-LMCZKHZV-DZf45Fbz.js} +1 -1
  9. package/dist/assets/channel-B3U5wFAT.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-Cyvi1AFH.js → chunk-2Q5K7J3B-Dn1spZYu.js} +1 -1
  11. package/dist/assets/{chunk-32BRIVSS-B1y5FpOd.js → chunk-32BRIVSS-pURGrJDk.js} +1 -1
  12. package/dist/assets/{chunk-5VM5RSS4-Qw-L7OoJ.js → chunk-5VM5RSS4-BY4_PV5H.js} +1 -1
  13. package/dist/assets/{chunk-EX3LRPZG-G_MZEji_.js → chunk-EX3LRPZG-DdELs1qP.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-DQFs7gjy.js → chunk-JWPE2WC7-_7ujgd_Q.js} +1 -1
  15. package/dist/assets/{chunk-MOJQB5TN-CQZQYMcz.js → chunk-MOJQB5TN-D-ky35G-.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-CmAda4sZ.js → chunk-RYQCIY6F-Dag_kVlO.js} +1 -1
  17. package/dist/assets/{chunk-V7JOEXUC-CbjBz-Ek.js → chunk-V7JOEXUC-BtewURat.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-Dvhgnm7E.js → chunk-VR4S4FIN-Dt9NZ67m.js} +1 -1
  19. package/dist/assets/{chunk-XXDRQBXY-CSdt06Hp.js → chunk-XXDRQBXY-DfdVhbmA.js} +1 -1
  20. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +1 -0
  21. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +1 -0
  22. package/dist/assets/{cose-bilkent-JH36ORCC-G8l05tXy.js → cose-bilkent-JH36ORCC-BieYif4o.js} +1 -1
  23. package/dist/assets/{cynefin-VYW2F7L2-C3n1PNJC.js → cynefin-VYW2F7L2-CT2BA6KE.js} +1 -1
  24. package/dist/assets/{cynefinDiagram-TSTJHNR4-C8N_CvKJ.js → cynefinDiagram-TSTJHNR4-Bh6exbyg.js} +1 -1
  25. package/dist/assets/{dagre-VKFMJZFB-B2pkwDhN.js → dagre-VKFMJZFB-aXMLSmQL.js} +1 -1
  26. package/dist/assets/{diagram-FQU43EPY-DPHD_t-1.js → diagram-FQU43EPY-Dr7JAOuQ.js} +1 -1
  27. package/dist/assets/{diagram-G47NLZAW-DGmHIZRr.js → diagram-G47NLZAW-DUvA3FQK.js} +1 -1
  28. package/dist/assets/{diagram-NH7WQ7WH-BlLWwKVt.js → diagram-NH7WQ7WH-BQUARqcu.js} +1 -1
  29. package/dist/assets/{diagram-OA4YK3LP-DEv704Re.js → diagram-OA4YK3LP-dDUc1zHi.js} +1 -1
  30. package/dist/assets/{diagram-WEI45ONY-Bj6PGUCe.js → diagram-WEI45ONY-B2h5Qlb1.js} +1 -1
  31. package/dist/assets/{ebnfDiagram-CCIWWBDH-Ch-icksT.js → ebnfDiagram-CCIWWBDH-DThRGupB.js} +1 -1
  32. package/dist/assets/{erDiagram-Q63AITRT-CI3lwEFA.js → erDiagram-Q63AITRT-BUCsprO2.js} +1 -1
  33. package/dist/assets/{flowDiagram-23GEKE2U-DcUANpAz.js → flowDiagram-23GEKE2U-DXtNNi6r.js} +1 -1
  34. package/dist/assets/{ganttDiagram-NO4QXBWP-Cy0Ie8KS.js → ganttDiagram-NO4QXBWP-D4zbBHh_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-IHSO6WYX-BswI52az.js → gitGraphDiagram-IHSO6WYX-DpoQws0W.js} +1 -1
  36. package/dist/assets/{index-B1H7-G2y.js → index-3ZH5eMcZ.js} +2 -2
  37. package/dist/assets/{infoDiagram-FWYZ7A6U-BBunqvr-.js → infoDiagram-FWYZ7A6U-Bbas2GAo.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-FXEZZL3T-Hm4zSDlM.js → ishikawaDiagram-FXEZZL3T-CzEB9fQS.js} +1 -1
  39. package/dist/assets/{journeyDiagram-5HDEW3XC-CIqcTFuO.js → journeyDiagram-5HDEW3XC-Bz8TCdz2.js} +1 -1
  40. package/dist/assets/{kanban-definition-HUTT4EX6-CB9jCXj_.js → kanban-definition-HUTT4EX6-tozrMoV_.js} +1 -1
  41. package/dist/assets/{linear-CsXqv70E.js → linear-Ck7gpa5N.js} +1 -1
  42. package/dist/assets/{mindmap-definition-LN4V7U3C-CpqH5F_P.js → mindmap-definition-LN4V7U3C-DTcHO0DJ.js} +1 -1
  43. package/dist/assets/{pegDiagram-2B236MQR-HpQyrDcm.js → pegDiagram-2B236MQR-CHiINrNy.js} +1 -1
  44. package/dist/assets/{pieDiagram-ENE6RG2P-xkcpHwnY.js → pieDiagram-ENE6RG2P-CfS4YFlR.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-ABIIQ3AL-kebZIOmu.js → quadrantDiagram-ABIIQ3AL-CadesS9w.js} +1 -1
  46. package/dist/assets/{railroadDiagram-RFXS5EU6-DNvGwWm9.js → railroadDiagram-RFXS5EU6-CgWEspBN.js} +1 -1
  47. package/dist/assets/{requirementDiagram-TGXJPOKE-D1lG1P7h.js → requirementDiagram-TGXJPOKE-bnI2zJeT.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-HTMAVEWB-DOEEcn52.js → sankeyDiagram-HTMAVEWB-YWKFgOGw.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-DBY2YBRQ-DynIVCk_.js → sequenceDiagram-DBY2YBRQ-BvkNOyF9.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-DJXFjUUL.js → sizeCapture-X5ZJPWSS-DlFPA2yO.js} +1 -1
  51. package/dist/assets/{stateDiagram-2N3HPSRC-OZ7trFh5.js → stateDiagram-2N3HPSRC-h8NIx0kQ.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +1 -0
  53. package/dist/assets/{swimlanes-5IMT3BWC-DULYAFmB.js → swimlanes-5IMT3BWC-CT5n22kG.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +8 -0
  55. package/dist/assets/{timeline-definition-FHXFAJF6-CzqqUDvt.js → timeline-definition-FHXFAJF6-D267GQFF.js} +1 -1
  56. package/dist/assets/{vennDiagram-L72KCM5P-CdKmtcFL.js → vennDiagram-L72KCM5P-Dj-wWLYG.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-EHGQE667-DknGwjCA.js → wardleyDiagram-EHGQE667-BxCeYxkG.js} +1 -1
  58. package/dist/assets/{xychartDiagram-FW5EYKEG-DdYyKfQ3.js → xychartDiagram-FW5EYKEG-DMFqWn7z.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/lib/idempotency.js +33 -8
  61. package/lib/judgment/schema.js +23 -0
  62. package/lib/judgment/store/index.js +49 -0
  63. package/lib/judgment/store/records.js +246 -0
  64. package/lib/judgment-gen.js +342 -0
  65. package/lib/judgment-write-guard.js +230 -0
  66. package/lib/judgment-writer.js +877 -0
  67. package/lib/lifecycle-modes.js +42 -0
  68. package/package.json +1 -1
  69. package/server/compose-mcp-tools.js +34 -0
  70. package/server/compose-mcp.js +125 -0
  71. package/server/mcp-tool-policy.js +1 -0
  72. package/dist/assets/channel-BzkmtaLy.js +0 -1
  73. package/dist/assets/classDiagram-OUVF2IWQ-BIvLcbex.js +0 -1
  74. package/dist/assets/classDiagram-v2-EOCWNBFH-BIvLcbex.js +0 -1
  75. package/dist/assets/stateDiagram-v2-6OUMAXLB-GcPRHAJT.js +0 -1
  76. package/dist/assets/swimlanesDiagram-G3AALYLV-BB0gCy01.js +0 -8
@@ -0,0 +1,246 @@
1
+ /**
2
+ * lib/judgment/store/records.js — the tracked-floor judgment canon store (S02).
3
+ *
4
+ * Records are canonical, git-tracked JSON under docs/judgment/records/
5
+ * (the feature.json precedent — NOT the gitignored vision store):
6
+ *
7
+ * positions/<slug>/r<N>.json append-only revision chain; highest N is current
8
+ * joints/<slug>.json one file per joint; state lives here
9
+ * predictions/<id>.json spawned by CONSTRUCT/commit events
10
+ * intents/<id>.json pending-intent records (intent-first transitions)
11
+ * ledger.jsonl append-only event stream
12
+ *
13
+ * Position status is DERIVED, never stored: a chain is `superseded` iff the
14
+ * latest revision of a non-retracted chain elsewhere names it in `supersedes`;
15
+ * `retracted` iff its own latest revision is a tombstone; `live` otherwise.
16
+ *
17
+ * All writes are atomic: `${path}.tmp.${process.pid}` + rename (the
18
+ * feature-json.js:87-90 idiom — the pid-less journal-writer variant is a
19
+ * known trap, do not copy it). The ledger append rewrites the whole stream
20
+ * through the same tmp+rename path so a crash can never leave a torn line.
21
+ */
22
+ import {
23
+ readFileSync, writeFileSync, renameSync, unlinkSync,
24
+ mkdirSync, readdirSync, existsSync,
25
+ } from 'node:fs';
26
+ import { join } from 'node:path';
27
+
28
+ function atomicWrite(path, content) {
29
+ const tmp = `${path}.tmp.${process.pid}`;
30
+ try {
31
+ writeFileSync(tmp, content);
32
+ renameSync(tmp, path);
33
+ } catch (err) {
34
+ try { unlinkSync(tmp); } catch { /* tmp may not exist */ }
35
+ throw err;
36
+ }
37
+ }
38
+
39
+ function readJson(path) {
40
+ try {
41
+ return JSON.parse(readFileSync(path, 'utf8'));
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ const REV_FILE_RE = /^r([1-9][0-9]*)\.json$/;
48
+
49
+ export class RecordsStore {
50
+ /** @param {string} cwd project root */
51
+ constructor(cwd) {
52
+ this.cwd = cwd;
53
+ this.root = join(cwd, 'docs', 'judgment', 'records');
54
+ }
55
+
56
+ /** Enrichment capabilities — the floor has none (W4 adds them via config, not here). */
57
+ capabilities() {
58
+ return new Set();
59
+ }
60
+
61
+ // ── positions ────────────────────────────────────────────────────────────
62
+
63
+ _positionDir(slug) {
64
+ return join(this.root, 'positions', slug);
65
+ }
66
+
67
+ _revNumbers(slug) {
68
+ const dir = this._positionDir(slug);
69
+ if (!existsSync(dir)) return [];
70
+ return readdirSync(dir)
71
+ .map((f) => REV_FILE_RE.exec(f))
72
+ .filter(Boolean)
73
+ .map((m) => Number(m[1]))
74
+ .sort((a, b) => a - b);
75
+ }
76
+
77
+ /**
78
+ * Append the next revision of a chain. Stamps `rev` (next N); revisions are
79
+ * immutable once written — this never overwrites an existing r<N>.json.
80
+ * @returns {{ rev: number, ref: string, path: string }}
81
+ */
82
+ writePositionRevision(record) {
83
+ const dir = this._positionDir(record.slug);
84
+ mkdirSync(dir, { recursive: true });
85
+ const revs = this._revNumbers(record.slug);
86
+ const rev = (revs.length ? revs[revs.length - 1] : 0) + 1;
87
+ const path = join(dir, `r${rev}.json`);
88
+ atomicWrite(path, JSON.stringify({ ...record, rev }, null, 2) + '\n');
89
+ return { rev, ref: `${record.slug}#r${rev}`, path };
90
+ }
91
+
92
+ /** Full chain, ascending by rev. */
93
+ readPositionChain(slug) {
94
+ return this._revNumbers(slug)
95
+ .map((n) => readJson(join(this._positionDir(slug), `r${n}.json`)))
96
+ .filter(Boolean);
97
+ }
98
+
99
+ readPositionRevision(slug, rev) {
100
+ return readJson(join(this._positionDir(slug), `r${rev}.json`));
101
+ }
102
+
103
+ listPositionSlugs() {
104
+ const dir = join(this.root, 'positions');
105
+ if (!existsSync(dir)) return [];
106
+ return readdirSync(dir, { withFileTypes: true })
107
+ .filter((d) => d.isDirectory())
108
+ .map((d) => d.name)
109
+ .sort();
110
+ }
111
+
112
+ /** Latest revision of a chain, or null. */
113
+ latestPositionRevision(slug) {
114
+ const revs = this._revNumbers(slug);
115
+ if (revs.length === 0) return null;
116
+ return this.readPositionRevision(slug, revs[revs.length - 1]);
117
+ }
118
+
119
+ /**
120
+ * Derived chain status: 'live' | 'superseded' | 'retracted' | null (no chain).
121
+ * Never stored anywhere — computed from the chains on every call.
122
+ */
123
+ derivePositionStatus(slug) {
124
+ const latest = this.latestPositionRevision(slug);
125
+ if (!latest) return null;
126
+ if (latest.retracted === true) return 'retracted';
127
+ for (const other of this.listPositionSlugs()) {
128
+ if (other === slug) continue;
129
+ const otherLatest = this.latestPositionRevision(other);
130
+ if (!otherLatest || otherLatest.retracted === true) continue;
131
+ const ref = otherLatest.supersedes;
132
+ if (typeof ref === 'string' && ref.startsWith(`${slug}#r`)) return 'superseded';
133
+ }
134
+ return 'live';
135
+ }
136
+
137
+ // ── joints ───────────────────────────────────────────────────────────────
138
+
139
+ _jointPath(slug) {
140
+ return join(this.root, 'joints', `${slug}.json`);
141
+ }
142
+
143
+ writeJoint(record) {
144
+ mkdirSync(join(this.root, 'joints'), { recursive: true });
145
+ atomicWrite(this._jointPath(record.slug), JSON.stringify(record, null, 2) + '\n');
146
+ return { slug: record.slug };
147
+ }
148
+
149
+ readJoint(slug) {
150
+ return readJson(this._jointPath(slug));
151
+ }
152
+
153
+ listJoints() {
154
+ const dir = join(this.root, 'joints');
155
+ if (!existsSync(dir)) return [];
156
+ return readdirSync(dir)
157
+ .filter((f) => f.endsWith('.json'))
158
+ .sort()
159
+ .map((f) => readJson(join(dir, f)))
160
+ .filter(Boolean);
161
+ }
162
+
163
+ // ── predictions ──────────────────────────────────────────────────────────
164
+
165
+ _predictionPath(id) {
166
+ return join(this.root, 'predictions', `${id}.json`);
167
+ }
168
+
169
+ writePrediction(record) {
170
+ mkdirSync(join(this.root, 'predictions'), { recursive: true });
171
+ atomicWrite(this._predictionPath(record.id), JSON.stringify(record, null, 2) + '\n');
172
+ return { id: record.id };
173
+ }
174
+
175
+ readPrediction(id) {
176
+ return readJson(this._predictionPath(id));
177
+ }
178
+
179
+ listPredictions({ status } = {}) {
180
+ const dir = join(this.root, 'predictions');
181
+ if (!existsSync(dir)) return [];
182
+ return readdirSync(dir)
183
+ .filter((f) => f.endsWith('.json'))
184
+ .sort()
185
+ .map((f) => readJson(join(dir, f)))
186
+ .filter(Boolean)
187
+ .filter((p) => (status ? p.status === status : true));
188
+ }
189
+
190
+ // ── ledger ───────────────────────────────────────────────────────────────
191
+
192
+ _ledgerPath() {
193
+ return join(this.root, 'ledger.jsonl');
194
+ }
195
+
196
+ /**
197
+ * Append one event. Rewrites the stream via tmp+rename (atomic — a crash
198
+ * mid-append can never leave a torn trailing line). Append-only: there is
199
+ * no update or delete surface on this store.
200
+ */
201
+ appendLedgerEvent(event) {
202
+ mkdirSync(this.root, { recursive: true });
203
+ const path = this._ledgerPath();
204
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
205
+ atomicWrite(path, existing + JSON.stringify(event) + '\n');
206
+ return { seq: existing.split('\n').filter(Boolean).length + 1 };
207
+ }
208
+
209
+ readLedgerEvents() {
210
+ const path = this._ledgerPath();
211
+ if (!existsSync(path)) return [];
212
+ return readFileSync(path, 'utf8')
213
+ .split('\n')
214
+ .filter(Boolean)
215
+ .map((line) => {
216
+ try { return JSON.parse(line); } catch { return null; }
217
+ })
218
+ .filter(Boolean);
219
+ }
220
+
221
+ // ── pending intents ──────────────────────────────────────────────────────
222
+
223
+ _intentPath(id) {
224
+ return join(this.root, 'intents', `${id}.json`);
225
+ }
226
+
227
+ persistIntent(intent) {
228
+ mkdirSync(join(this.root, 'intents'), { recursive: true });
229
+ atomicWrite(this._intentPath(intent.id), JSON.stringify(intent, null, 2) + '\n');
230
+ return { id: intent.id };
231
+ }
232
+
233
+ readIntents() {
234
+ const dir = join(this.root, 'intents');
235
+ if (!existsSync(dir)) return [];
236
+ return readdirSync(dir)
237
+ .filter((f) => f.endsWith('.json'))
238
+ .sort()
239
+ .map((f) => readJson(join(dir, f)))
240
+ .filter(Boolean);
241
+ }
242
+
243
+ clearIntent(id) {
244
+ try { unlinkSync(this._intentPath(id)); } catch { /* already cleared */ }
245
+ }
246
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * lib/judgment-gen.js — judgment projections (S04).
3
+ *
4
+ * records → REGISTER.md, LEDGER.md, OBJECTIVE.md, positions/<slug>.md, and
5
+ * the OKF bundle root index.md. Projections are PURE OUTPUT: generation
6
+ * reads only records — never an existing projection — so any hand-edit is
7
+ * overwritten by the next regeneration and there are no preserved sections
8
+ * (curated prose is `note` records rendered at their anchors).
9
+ *
10
+ * Output is a deterministic function of the records (no wall-clock in the
11
+ * bytes), which is what makes the fixed-point roundtrip guard meaningful:
12
+ * regen of the generator's own output is byte-identical.
13
+ *
14
+ * OKF (design Decision 8): per-item files carry frontmatter type/title/
15
+ * timestamp plus the smartmemory { reference: true, origin } extension;
16
+ * `resource` is emitted ONLY when a record carries a real provider id AND
17
+ * an enrichment workspace (team_id) is configured — a made-up resource
18
+ * would be a dangling identity claim. The bundle root carries
19
+ * okf_version "0.1" and no type (okf.ts renderOkf rule).
20
+ */
21
+ import {
22
+ readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, readdirSync,
23
+ } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { createJudgmentStore } from './judgment/store/index.js';
26
+
27
+ function atomicWrite(path, content) {
28
+ const tmp = `${path}.tmp.${process.pid}`;
29
+ try {
30
+ writeFileSync(tmp, content);
31
+ renameSync(tmp, path);
32
+ } catch (err) {
33
+ try { unlinkSync(tmp); } catch { /* tmp may not exist */ }
34
+ throw err;
35
+ }
36
+ }
37
+
38
+ function smartmemoryTeamId(cwd) {
39
+ try {
40
+ const config = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf8'));
41
+ return config?.judgment?.enrichment?.smartmemory?.team_id ?? null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // ── snapshot ────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Load everything projection-relevant from the store into a plain snapshot,
51
+ * so the generator core stays pure.
52
+ */
53
+ function loadSnapshot(cwd) {
54
+ const store = createJudgmentStore(cwd);
55
+ const positions = store.listPositionSlugs().map((slug) => ({
56
+ slug,
57
+ chain: store.readPositionChain(slug),
58
+ status: store.derivePositionStatus(slug),
59
+ }));
60
+ return {
61
+ positions,
62
+ joints: store.listJoints(),
63
+ ledger: store.readLedgerEvents(),
64
+ teamId: smartmemoryTeamId(cwd),
65
+ };
66
+ }
67
+
68
+ // ── rendering helpers ───────────────────────────────────────────────────────
69
+
70
+ function anchoredNotes(ledger, anchor) {
71
+ return ledger.filter((e) => e.kind === 'note' && e.anchor === anchor);
72
+ }
73
+
74
+ function renderNotes(notes) {
75
+ return notes.map((n) => `> **${n.title}** — ${n.body ?? ''}`.trimEnd()).join('\n\n');
76
+ }
77
+
78
+ function okfFrontmatter({ type, title, timestamp, resource }) {
79
+ const lines = ['---'];
80
+ if (type) lines.push(`type: ${type}`);
81
+ else lines.push('okf_version: "0.1"');
82
+ lines.push(`title: ${title}`);
83
+ if (timestamp) lines.push(`timestamp: "${timestamp}"`);
84
+ if (resource) lines.push(`resource: "${resource}"`);
85
+ lines.push('smartmemory:');
86
+ lines.push(' reference: true');
87
+ lines.push(' origin: compose-projection');
88
+ lines.push('---');
89
+ return lines.join('\n');
90
+ }
91
+
92
+ function buildResource(teamId, providerId) {
93
+ if (!teamId || !providerId) return null;
94
+ return `smartmemory://${encodeURIComponent(teamId)}/${encodeURIComponent(providerId)}`;
95
+ }
96
+
97
+ function renderClaim(claim) {
98
+ const lines = [`- **${claim.id}** \`[${claim.grounding}]\`${claim.owner_locked ? ' `[owner-locked]`' : ''} ${claim.text}`];
99
+ if (Array.isArray(claim.supports) && claim.supports.length > 0) {
100
+ lines.push(` - supports: ${claim.supports.join(', ')}`);
101
+ }
102
+ if (claim.elicitation) {
103
+ lines.push(` - elicitation: asked "${claim.elicitation.asked}" (answered ${claim.elicitation.answered_at}, ref ${claim.elicitation.answer_ref})`);
104
+ }
105
+ return lines.join('\n');
106
+ }
107
+
108
+ function renderPosition(position, snapshot) {
109
+ const { slug, chain, status } = position;
110
+ const current = chain[chain.length - 1];
111
+ const resource = buildResource(snapshot.teamId, current?.provider_ids?.smartmemory);
112
+ const parts = [okfFrontmatter({
113
+ type: 'position',
114
+ title: slug,
115
+ timestamp: current?.provenance?.written_at,
116
+ resource,
117
+ })];
118
+ parts.push('');
119
+ parts.push(`# ${slug}`);
120
+ parts.push('');
121
+ parts.push(`**Status:** ${status} · **Conviction:** ${current.conviction?.level ?? '—'} (${current.conviction?.source ?? '—'})`);
122
+ if (current.retracted) {
123
+ parts.push('');
124
+ parts.push('**Retracted.** The latest revision is a tombstone.');
125
+ }
126
+ if (current.claims?.length) {
127
+ parts.push('');
128
+ parts.push(`## Claims (r${current.rev})`);
129
+ parts.push('');
130
+ for (const claim of current.claims) parts.push(renderClaim(claim));
131
+ }
132
+ if (current.rejected_alternatives?.length) {
133
+ parts.push('');
134
+ parts.push('## Rejected alternatives');
135
+ parts.push('');
136
+ for (const alt of current.rejected_alternatives) parts.push(`- ${alt.what} — ${alt.why}`);
137
+ }
138
+ parts.push('');
139
+ parts.push('## History');
140
+ parts.push('');
141
+ for (const rev of chain) {
142
+ const marks = [];
143
+ if (rev.supersedes) marks.push(`supersedes ${rev.supersedes}`);
144
+ if (rev.retracted) marks.push('tombstone');
145
+ parts.push(`- r${rev.rev} — ${rev.provenance?.written_at ?? 'unknown'}${marks.length ? ` (${marks.join(', ')})` : ''}`);
146
+ }
147
+ const notes = anchoredNotes(snapshot.ledger, `position:${slug}`);
148
+ if (notes.length) {
149
+ parts.push('');
150
+ parts.push(renderNotes(notes));
151
+ }
152
+ return parts.join('\n') + '\n';
153
+ }
154
+
155
+ function renderJointSection(joint, snapshot) {
156
+ const parts = [`## ${joint.slug}`];
157
+ parts.push('');
158
+ parts.push(`- **Question:** ${joint.question}`);
159
+ parts.push(`- **If true:** ${joint.branch_true}`);
160
+ parts.push(`- **If false:** ${joint.branch_false}`);
161
+ parts.push(`- **Method:** ${joint.resolve_by} · cost ${joint.cost} · rank ${joint.rank}`);
162
+ parts.push(`- **State:** ${joint.state}`);
163
+ if (joint.flags?.length) parts.push(`- **Flags:** ${joint.flags.join(', ')}`);
164
+ if (joint.ext) {
165
+ parts.push(joint.ext.judgment_dispatch
166
+ ? `- **Ext:** judgment-dispatch — ${joint.ext.reason}`
167
+ : `- **Ext:** sharpened "${joint.ext.sharpened_question}" · bar: ${joint.ext.bar} · falsifier: ${joint.ext.falsifier}`);
168
+ }
169
+ if (joint.straddle) {
170
+ parts.push(`- **Straddle:** signal ${joint.straddle.discriminating_signal} · kill: ${joint.straddle.kill_criteria}`);
171
+ }
172
+ if (joint.resolution) {
173
+ const r = joint.resolution;
174
+ const detail = r.evidence ?? r.learned ?? r.reason ?? r.why ?? '';
175
+ parts.push(`- **Resolution:** ${r.outcome}${detail ? ` — ${typeof detail === 'string' ? detail : JSON.stringify(detail)}` : ''}`);
176
+ if (r.outcome === 'inconclusive' && r.would_have_settled) {
177
+ parts.push(` - would have settled: ${r.would_have_settled}`);
178
+ }
179
+ if (r.ext_result) {
180
+ parts.push(` - ext result: ${r.ext_result.outcome} (${r.ext_result.found_or_provoked}) — ${r.ext_result.search_record}`);
181
+ }
182
+ }
183
+ if (joint.dissolution) {
184
+ parts.push(`- **Dissolved into:** ${joint.dissolution.decomposed_into.join(', ')}`);
185
+ }
186
+ const notes = anchoredNotes(snapshot.ledger, `joint:${joint.slug}`);
187
+ if (notes.length) {
188
+ parts.push('');
189
+ parts.push(renderNotes(notes));
190
+ }
191
+ return parts.join('\n');
192
+ }
193
+
194
+ const RANK_ORDER = { high: 0, medium: 1 };
195
+
196
+ function renderRegister(snapshot) {
197
+ const parts = ['# Judgment Register'];
198
+ const headerNotes = anchoredNotes(snapshot.ledger, 'register-header');
199
+ if (headerNotes.length) {
200
+ parts.push('');
201
+ parts.push(renderNotes(headerNotes));
202
+ }
203
+ const joints = [...snapshot.joints].sort((a, b) =>
204
+ (RANK_ORDER[a.rank] - RANK_ORDER[b.rank]) || a.slug.localeCompare(b.slug));
205
+ for (const joint of joints) {
206
+ parts.push('');
207
+ parts.push(renderJointSection(joint, snapshot));
208
+ }
209
+ const footerNotes = anchoredNotes(snapshot.ledger, 'register-footer');
210
+ if (footerNotes.length) {
211
+ parts.push('');
212
+ parts.push(renderNotes(footerNotes));
213
+ }
214
+ return parts.join('\n') + '\n';
215
+ }
216
+
217
+ const EVENT_DETAIL_KEYS = [
218
+ 'refs', 'rejected', 'conviction', 'trigger', 'open_joints', 'prediction',
219
+ 'disposition', 'recall_verdict', 'attribution', 'prediction_ref',
220
+ 'prediction_grade', 'reason', 'rank_change', 'elicitation',
221
+ ];
222
+
223
+ function renderLedger(snapshot) {
224
+ const parts = ['# Judgment Ledger'];
225
+ const headerNotes = anchoredNotes(snapshot.ledger, 'ledger-header');
226
+ if (headerNotes.length) {
227
+ parts.push('');
228
+ parts.push(renderNotes(headerNotes));
229
+ }
230
+ snapshot.ledger.forEach((event, i) => {
231
+ parts.push('');
232
+ parts.push(`## ${i + 1}. ${event.kind}: ${event.title}`);
233
+ if (event.body) {
234
+ parts.push('');
235
+ parts.push(event.body);
236
+ }
237
+ const details = EVENT_DETAIL_KEYS
238
+ .filter((k) => event[k] !== undefined && !(Array.isArray(event[k]) && event[k].length === 0))
239
+ .map((k) => `- ${k}: ${typeof event[k] === 'string' ? event[k] : JSON.stringify(event[k])}`);
240
+ if (event.anchor) details.push(`- anchor: ${event.anchor}`);
241
+ if (details.length) {
242
+ parts.push('');
243
+ parts.push(details.join('\n'));
244
+ }
245
+ parts.push('');
246
+ parts.push(`*${event.provenance?.written_at ?? ''}${event.provenance?.via ? ` · via ${event.provenance.via}` : ''}*`);
247
+ });
248
+ return parts.join('\n') + '\n';
249
+ }
250
+
251
+ function renderObjective(snapshot) {
252
+ const objective = snapshot.positions.find((p) => p.slug === 'objective');
253
+ if (objective) return renderPosition(objective, snapshot);
254
+ return [
255
+ okfFrontmatter({ type: 'position', title: 'objective' }),
256
+ '',
257
+ '# objective',
258
+ '',
259
+ 'No objective recorded.',
260
+ ].join('\n') + '\n';
261
+ }
262
+
263
+ function renderIndex(snapshot) {
264
+ const parts = [okfFrontmatter({ type: null, title: 'Judgment canon' })];
265
+ parts.push('');
266
+ parts.push('# Judgment canon (generated)');
267
+ parts.push('');
268
+ parts.push('All files in this bundle are projections of `records/` — regenerated, never hand-edited.');
269
+ parts.push('');
270
+ parts.push('- [Register](REGISTER.md)');
271
+ parts.push('- [Ledger](LEDGER.md)');
272
+ parts.push('- [Objective](OBJECTIVE.md)');
273
+ if (snapshot.positions.length) {
274
+ parts.push('');
275
+ parts.push('## Positions');
276
+ parts.push('');
277
+ for (const p of snapshot.positions) {
278
+ parts.push(`- [${p.slug}](positions/${p.slug}.md) — ${p.status}`);
279
+ }
280
+ }
281
+ return parts.join('\n') + '\n';
282
+ }
283
+
284
+ // ── public surface ──────────────────────────────────────────────────────────
285
+
286
+ /**
287
+ * Pure generator core: snapshot → { relPath: content }. Exported for tests;
288
+ * `now` is accepted for interface stability but the output deliberately
289
+ * contains no wall-clock bytes (fixed-point requirement).
290
+ */
291
+ export function generateFromRecords(snapshot, { now } = {}) { // eslint-disable-line no-unused-vars
292
+ const files = {
293
+ 'docs/judgment/REGISTER.md': renderRegister(snapshot),
294
+ 'docs/judgment/LEDGER.md': renderLedger(snapshot),
295
+ 'docs/judgment/OBJECTIVE.md': renderObjective(snapshot),
296
+ 'docs/judgment/index.md': renderIndex(snapshot),
297
+ };
298
+ for (const position of snapshot.positions) {
299
+ files[`docs/judgment/positions/${position.slug}.md`] = renderPosition(position, snapshot);
300
+ }
301
+ return files;
302
+ }
303
+
304
+ /**
305
+ * Regenerate every projection from records on disk. Atomic per file.
306
+ * @returns {{ files: string[] }} relative paths written
307
+ */
308
+ export function regenerateProjections(cwd) {
309
+ const files = generateFromRecords(loadSnapshot(cwd));
310
+ for (const [rel, content] of Object.entries(files)) {
311
+ const path = join(cwd, rel);
312
+ mkdirSync(join(path, '..'), { recursive: true });
313
+ atomicWrite(path, content);
314
+ }
315
+ return { files: Object.keys(files) };
316
+ }
317
+
318
+ /**
319
+ * Fixed-point roundtrip guard: regenerate in memory and compare byte-for-byte
320
+ * against what is on disk. Any drift (hand-edit, stale projection) fails.
321
+ * @returns {{ fixedPoint: boolean, diffs: string[] }}
322
+ */
323
+ export function checkProjectionRoundtrip(cwd) {
324
+ const files = generateFromRecords(loadSnapshot(cwd));
325
+ const diffs = [];
326
+ for (const [rel, content] of Object.entries(files)) {
327
+ const path = join(cwd, rel);
328
+ const onDisk = existsSync(path) ? readFileSync(path, 'utf8') : null;
329
+ if (onDisk !== content) diffs.push(rel);
330
+ }
331
+ // Orphan detection: a projection file the generator no longer emits (stale
332
+ // position projection, leftover from an older generator) is drift too.
333
+ const positionsDir = join(cwd, 'docs', 'judgment', 'positions');
334
+ if (existsSync(positionsDir)) {
335
+ for (const f of readdirSync(positionsDir)) {
336
+ if (!f.endsWith('.md')) continue;
337
+ const rel = `docs/judgment/positions/${f}`;
338
+ if (!(rel in files)) diffs.push(`${rel} (orphan — not emitted by the generator)`);
339
+ }
340
+ }
341
+ return { fixedPoint: diffs.length === 0, diffs };
342
+ }