@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
@@ -24,6 +24,14 @@ export const HOOK_MARKERS = {
24
24
  'pre-push': '# Compose pre-push hook —',
25
25
  };
26
26
 
27
+ /**
28
+ * Current baked versions for hooks whose content contract is versioned.
29
+ * Post-commit remains unversioned until its template changes.
30
+ */
31
+ export const HOOK_VERSIONS = {
32
+ 'pre-push': '1',
33
+ };
34
+
27
35
  /** The hook types Compose manages, in display order. */
28
36
  export const HOOK_TYPE_LIST = Object.keys(HOOK_MARKERS);
29
37
 
@@ -33,6 +41,12 @@ export function extractBakedWorkspaceId(content) {
33
41
  return m ? m[1] : null;
34
42
  }
35
43
 
44
+ /** Pull the baked HOOK_VERSION="..." value out of hook content, or null. */
45
+ export function extractBakedHookVersion(content) {
46
+ const m = content.match(/^HOOK_VERSION="([^"]*)"$/m);
47
+ return m ? m[1] : null;
48
+ }
49
+
36
50
  /**
37
51
  * Compute the raw status facts for every managed hook type.
38
52
  *
@@ -55,15 +69,19 @@ export function computeHooksStatus({ projectRoot, expectedWsId = null, composeNo
55
69
 
56
70
  function computeOne({ type, hooksDir, expectedWsId, composeNode, composeBin }) {
57
71
  const marker = HOOK_MARKERS[type];
72
+ const expectedVersion = HOOK_VERSIONS[type] ?? null;
58
73
  const dest = join(hooksDir, type);
59
74
 
60
75
  const empty = {
61
76
  reason: null,
62
77
  bakedWorkspace: null,
78
+ bakedVersion: null,
79
+ expectedVersion,
63
80
  expectedWorkspace: expectedWsId ?? null,
64
81
  wsVerified: false,
65
82
  nodeMatch: false,
66
83
  binMatch: false,
84
+ versionMatch: false,
67
85
  };
68
86
 
69
87
  if (!existsSync(dest)) return { state: 'absent', ...empty };
@@ -75,26 +93,33 @@ function computeOne({ type, hooksDir, expectedWsId, composeNode, composeBin }) {
75
93
  const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`);
76
94
  const hasRawToken = content.includes('__COMPOSE_WORKSPACE_ID__');
77
95
  const bakedWorkspace = extractBakedWorkspaceId(content);
96
+ const bakedVersion = extractBakedHookVersion(content);
97
+ const versionMatch = expectedVersion == null || bakedVersion === expectedVersion;
78
98
  // Mirror of statusOne: raw token never matches; a known expected id must match
79
99
  // the baked value; an unknown (null) expected id is treated leniently as a match.
80
100
  const wsMatch = hasRawToken ? false : expectedWsId ? content.includes(`COMPOSE_WORKSPACE_ID="${expectedWsId}"`) : true;
81
101
 
82
102
  const base = {
83
103
  bakedWorkspace,
104
+ bakedVersion,
105
+ expectedVersion,
84
106
  expectedWorkspace: expectedWsId ?? null,
85
107
  wsVerified: expectedWsId != null,
86
108
  nodeMatch,
87
109
  binMatch,
110
+ versionMatch,
88
111
  };
89
112
 
90
- if (nodeMatch && binMatch && wsMatch && !hasRawToken) {
113
+ if (nodeMatch && binMatch && wsMatch && !hasRawToken && versionMatch) {
91
114
  return { state: 'installed-current', reason: null, ...base };
92
115
  }
93
116
  const reason = hasRawToken
94
117
  ? 'MISSING_WORKSPACE_ID'
95
118
  : expectedWsId && !wsMatch
96
119
  ? 'STALE_WORKSPACE_ID'
97
- : 'stale paths';
120
+ : !nodeMatch || !binMatch
121
+ ? 'stale paths'
122
+ : 'HOOK_VERSION_DRIFT';
98
123
  return { state: 'installed-stale', reason, ...base };
99
124
  }
100
125
 
@@ -118,6 +143,7 @@ export function formatHookStatusLines(type, s, { composeNode, composeBin }) {
118
143
  // installed-stale
119
144
  const lines = [`${type}: installed (${s.reason} — re-run install)`];
120
145
  if (s.reason === 'STALE_WORKSPACE_ID') lines.push(` expected COMPOSE_WORKSPACE_ID="${s.expectedWorkspace}"`);
146
+ if (s.reason === 'HOOK_VERSION_DRIFT') lines.push(` expected HOOK_VERSION="${s.expectedVersion}"`);
121
147
  if (!s.nodeMatch) lines.push(` expected COMPOSE_NODE="${composeNode}"`);
122
148
  if (!s.binMatch) lines.push(` expected COMPOSE_BIN="${composeBin}"`);
123
149
  return lines;
@@ -126,10 +152,13 @@ export function formatHookStatusLines(type, s, { composeNode, composeBin }) {
126
152
  /**
127
153
  * @typedef {object} HookStatusRaw
128
154
  * @property {'absent'|'foreign'|'installed-current'|'installed-stale'} state
129
- * @property {null|'MISSING_WORKSPACE_ID'|'STALE_WORKSPACE_ID'|'stale paths'} reason
155
+ * @property {null|'MISSING_WORKSPACE_ID'|'STALE_WORKSPACE_ID'|'stale paths'|'HOOK_VERSION_DRIFT'} reason
130
156
  * @property {string|null} bakedWorkspace
157
+ * @property {string|null} bakedVersion
158
+ * @property {string|null} expectedVersion
131
159
  * @property {string|null} expectedWorkspace
132
160
  * @property {boolean} wsVerified
133
161
  * @property {boolean} nodeMatch
134
162
  * @property {boolean} binMatch
163
+ * @property {boolean} versionMatch
135
164
  */
@@ -18,7 +18,13 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
18
18
  import { join, dirname } from 'path';
19
19
 
20
20
  const MAX_ENTRIES = 1000;
21
- const LOCK_TIMEOUT_MS = 5000;
21
+ const LOCK_TIMEOUT_MS = 5000; // acquisition wait
22
+ // Steal threshold — deliberately larger than the wait: the heartbeat (1s,
23
+ // event-loop-scheduled) can be silenced by a SYNCHRONOUS computeFn, so a
24
+ // live lock is only stealable after a sync block longer than this. 20s of
25
+ // contiguous sync blocking is the documented bound; a fresh crash costs
26
+ // waiters up to 20s of retryable lock-timeout errors before recovery.
27
+ const LOCK_STALE_MS = 20000;
22
28
  const LOCK_RETRY_MS = 25;
23
29
 
24
30
  function cacheFile(cwd) {
@@ -35,28 +41,47 @@ function ensureDir(file) {
35
41
 
36
42
  /**
37
43
  * Acquire an advisory lock by creating a directory (atomic on POSIX). Caller
38
- * is responsible for releasing via the returned function. Stale locks older
39
- * than LOCK_TIMEOUT_MS are forcibly cleared so a crashed prior holder can't
40
- * deadlock the next caller.
44
+ * is responsible for releasing via the returned function.
45
+ *
46
+ * The lock is HEARTBEATED (COMP-JUDGMENT-WRITER hardening): the holder
47
+ * refreshes the dir mtime every second, so only a lock whose holder has died
48
+ * (heartbeat silent for LOCK_TIMEOUT_MS) is ever stealable — a live compute
49
+ * that legitimately outlasts the stale threshold (e.g. a judgment transition
50
+ * spanning a 10s Stratum guard call) keeps its lock. An owner token makes
51
+ * release safe after a post-crash steal: a holder never removes a lock it no
52
+ * longer owns. (A steal requires LOCK_TIMEOUT_MS of MISSED heartbeats, which
53
+ * a live holder cannot produce — so token-check-then-remove races only exist
54
+ * against already-dead holders, where there is no releaser to race.)
41
55
  */
42
56
  async function acquireLock(cwd) {
43
57
  const path = lockFile(cwd);
44
58
  ensureDir(path);
59
+ const { statSync, utimesSync } = await import('fs');
60
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
61
+ const ownerFile = join(path, 'owner');
45
62
 
46
63
  const start = Date.now();
47
64
  // eslint-disable-next-line no-constant-condition
48
65
  while (true) {
49
66
  try {
50
67
  mkdirSync(path);
68
+ writeFileSync(ownerFile, token);
69
+ const heartbeat = setInterval(() => {
70
+ try { utimesSync(path, new Date(), new Date()); } catch { /* lock gone — release will no-op */ }
71
+ }, 1000);
72
+ heartbeat.unref?.();
51
73
  return () => {
52
- try { rmSync(path, { recursive: true, force: true }); } catch { /* best-effort */ }
74
+ clearInterval(heartbeat);
75
+ try {
76
+ if (readFileSync(ownerFile, 'utf-8') === token) rmSync(path, { recursive: true, force: true });
77
+ } catch { /* not ours anymore — leave it */ }
53
78
  };
54
79
  } catch (err) {
55
80
  if (err.code !== 'EEXIST') throw err;
56
- // Stale lock recovery: if older than timeout, clear and retry.
81
+ // Stale lock recovery: heartbeat silent longer than the stale bound → holder died.
57
82
  try {
58
- const { mtimeMs } = (await import('fs')).statSync(path);
59
- if (Date.now() - mtimeMs > LOCK_TIMEOUT_MS) {
83
+ const { mtimeMs } = statSync(path);
84
+ if (Date.now() - mtimeMs > LOCK_STALE_MS) {
60
85
  rmSync(path, { recursive: true, force: true });
61
86
  continue;
62
87
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * lib/judgment/schema.js — judgment-record contract loader (S01).
3
+ *
4
+ * Exposes the contract path and a memoized SchemaValidator instance. The
5
+ * schema carries an $id, which the definitions path of SchemaValidator
6
+ * requires (server/schema-validator.js:52). Callers validate per-kind via
7
+ * `getJudgmentValidator().validate('<definition>', obj)`.
8
+ */
9
+ import { dirname, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { SchemaValidator } from '../../server/schema-validator.js';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ export const JUDGMENT_SCHEMA_PATH = resolve(__dirname, '../../contracts/judgment-record.schema.json');
16
+
17
+ // Memoized — the contract is static for the life of the process.
18
+ let _validator = null;
19
+
20
+ export function getJudgmentValidator() {
21
+ if (!_validator) _validator = new SchemaValidator(JUDGMENT_SCHEMA_PATH);
22
+ return _validator;
23
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * lib/judgment/store/index.js — judgment canon provider registry (S02).
3
+ *
4
+ * Exactly one configured canon provider (design Decision 1/5): selection is
5
+ * read from `.compose/compose.json#judgment.provider`, default 'records'
6
+ * (the tracked floor). 'smartmemory' is the checkpoint-store-precedent seam:
7
+ * it throws NOT_IMPLEMENTED AT SELECTION — no stub object is ever returned.
8
+ * W4's SmartMemory integration is an enrichment emitter behind the SEPARATE
9
+ * `judgment.enrichment.smartmemory` key, never a canon provider.
10
+ *
11
+ * `capabilities()` lives on each backend (records ⇒ empty set), not here.
12
+ */
13
+ import { readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { RecordsStore } from './records.js';
16
+
17
+ const EFFECTIVE_STORES = new WeakSet();
18
+
19
+ function configuredProvider(cwd) {
20
+ try {
21
+ const config = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf8'));
22
+ return config?.judgment?.provider ?? 'records';
23
+ } catch {
24
+ return 'records';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Construct the configured judgment canon store for a project root.
30
+ * @param {string} cwd project root
31
+ * @returns {RecordsStore}
32
+ */
33
+ export function createJudgmentStore(cwd) {
34
+ const provider = configuredProvider(cwd);
35
+ switch (provider) {
36
+ case 'records':
37
+ return new RecordsStore(cwd);
38
+
39
+ case 'smartmemory':
40
+ // A SmartMemory CANON provider would need the full store contract plus
41
+ // an owner-ruled one-time import (Decision 1). Not built; selection
42
+ // fails loudly rather than returning a partial stub.
43
+ throw Object.assign(
44
+ new Error("SmartMemory judgment canon provider not implemented — v1 canon is 'records'; SmartMemory ships as enrichment (judgment.enrichment.smartmemory) in W4"),
45
+ { code: 'NOT_IMPLEMENTED' },
46
+ );
47
+
48
+ default:
49
+ throw new Error(`Unknown judgment provider '${provider}'. Valid ids: records, smartmemory.`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Construct a read-only view that excludes whole artifacts attributed to an
55
+ * intent which was pending when the view was created.
56
+ *
57
+ * The intent set is deliberately snapshotted once. Record files and ledger
58
+ * lines are filtered only by top-level provenance; nested intent attribution
59
+ * belongs to the containing record and is not recursively hidden.
60
+ *
61
+ * @param {RecordsStore} rawStore
62
+ */
63
+ export function effectiveStore(rawStore) {
64
+ if (EFFECTIVE_STORES.has(rawStore)) return rawStore;
65
+
66
+ const pendingIntents = rawStore.readIntents();
67
+ const pendingIds = new Set(pendingIntents.map((intent) => intent.id));
68
+ const isVisible = (record) => (
69
+ record != null && !pendingIds.has(record.provenance?.intent_id)
70
+ );
71
+ const visibleOrNull = (record) => (isVisible(record) ? record : null);
72
+
73
+ const adapter = {
74
+ capabilities() {
75
+ return rawStore.capabilities();
76
+ },
77
+
78
+ readPositionRevision(slug, rev) {
79
+ return visibleOrNull(rawStore.readPositionRevision(slug, rev));
80
+ },
81
+
82
+ readPositionChain(slug) {
83
+ return rawStore.readPositionChain(slug)
84
+ .map((record) => adapter.readPositionRevision(slug, record.rev))
85
+ .filter(Boolean);
86
+ },
87
+
88
+ latestPositionRevision(slug) {
89
+ const chain = adapter.readPositionChain(slug);
90
+ return chain.length === 0 ? null : chain[chain.length - 1];
91
+ },
92
+
93
+ listPositionSlugs() {
94
+ return rawStore.listPositionSlugs()
95
+ .filter((slug) => adapter.readPositionChain(slug).length > 0);
96
+ },
97
+
98
+ derivePositionStatus(slug) {
99
+ const latest = adapter.latestPositionRevision(slug);
100
+ if (!latest) return null;
101
+ if (latest.retracted === true) return 'retracted';
102
+ for (const other of adapter.listPositionSlugs()) {
103
+ if (other === slug) continue;
104
+ const otherLatest = adapter.latestPositionRevision(other);
105
+ if (!otherLatest || otherLatest.retracted === true) continue;
106
+ const ref = otherLatest.supersedes;
107
+ if (typeof ref === 'string' && ref.startsWith(`${slug}#r`)) {
108
+ return 'superseded';
109
+ }
110
+ }
111
+ return 'live';
112
+ },
113
+
114
+ readJoint(slug) {
115
+ return visibleOrNull(rawStore.readJoint(slug));
116
+ },
117
+
118
+ listJoints() {
119
+ return rawStore.listJoints()
120
+ .map((record) => adapter.readJoint(record.slug))
121
+ .filter(Boolean);
122
+ },
123
+
124
+ readPrediction(id) {
125
+ return visibleOrNull(rawStore.readPrediction(id));
126
+ },
127
+
128
+ listPredictions({ status } = {}) {
129
+ return rawStore.listPredictions()
130
+ .map((record) => adapter.readPrediction(record.id))
131
+ .filter(Boolean)
132
+ .filter((record) => (status ? record.status === status : true));
133
+ },
134
+
135
+ readLedgerEvents() {
136
+ return rawStore.readLedgerEvents().filter(isVisible);
137
+ },
138
+
139
+ readPerson(slug) {
140
+ return visibleOrNull(rawStore.readPerson(slug));
141
+ },
142
+
143
+ listPeople() {
144
+ return rawStore.listPeople()
145
+ .map((record) => adapter.readPerson(record.slug))
146
+ .filter(Boolean);
147
+ },
148
+
149
+ readSituationEntity(slug) {
150
+ return visibleOrNull(rawStore.readSituationEntity(slug));
151
+ },
152
+
153
+ listSituationEntities() {
154
+ return rawStore.listSituationEntities()
155
+ .map((record) => adapter.readSituationEntity(record.slug))
156
+ .filter(Boolean);
157
+ },
158
+
159
+ readGoalVersion(version) {
160
+ return visibleOrNull(rawStore.readGoalVersion(version));
161
+ },
162
+
163
+ readGoalChain() {
164
+ return rawStore.readGoalChain()
165
+ .map((record) => adapter.readGoalVersion(record.version))
166
+ .filter(Boolean);
167
+ },
168
+
169
+ latestGoalVersion() {
170
+ const chain = adapter.readGoalChain();
171
+ return chain.length === 0 ? null : chain[chain.length - 1];
172
+ },
173
+
174
+ readGoalState() {
175
+ const raw = rawStore.readGoalState();
176
+ // Records-atomic guarantee (C3): while a goal migration is pending, a
177
+ // state.json the migration already wrote (attributed to that intent)
178
+ // must read back as the captured PREIMAGE, not as absence. Every other
179
+ // family hides an attributed record as null; goal state instead
180
+ // substitutes the pre-migration bytes the intent carries.
181
+ const migration = pendingIntents.find((intent) => intent.kind === 'goal_migration');
182
+ if (migration && raw?.provenance?.intent_id === migration.id) {
183
+ return migration.payload?.goal_state_preimage?.record ?? null;
184
+ }
185
+ return visibleOrNull(raw);
186
+ },
187
+
188
+ hasPendingIntentKind(kind) {
189
+ return pendingIntents.some((intent) => intent.kind === kind);
190
+ },
191
+ };
192
+
193
+ EFFECTIVE_STORES.add(adapter);
194
+ return Object.freeze(adapter);
195
+ }
196
+
197
+ /**
198
+ * True once a published goal chain exists and no goal migration is pending.
199
+ * Accepts either a raw RecordsStore or an already-effective view.
200
+ */
201
+ export function goalCutoverComplete(store) {
202
+ const effective = EFFECTIVE_STORES.has(store) ? store : effectiveStore(store);
203
+ return (
204
+ effective.readGoalChain().length > 0
205
+ && !effective.hasPendingIntentKind('goal_migration')
206
+ );
207
+ }