@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
package/lib/hooks-status.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
*/
|
package/lib/idempotency.js
CHANGED
|
@@ -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.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
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:
|
|
81
|
+
// Stale lock recovery: heartbeat silent longer than the stale bound → holder died.
|
|
57
82
|
try {
|
|
58
|
-
const { mtimeMs } =
|
|
59
|
-
if (Date.now() - mtimeMs >
|
|
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
|
+
}
|