@smartmemory/compose 0.3.6-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 +7 -0
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/judgment-record.schema.json +425 -4
- 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-DZf45Fbz.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
- package/dist/assets/channel-qVK_qn4E.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.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-BieYif4o.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-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.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-Ck7gpa5N.js → linear-AvSTWMqx.js} +1 -1
- package/dist/assets/min-QBM8H4xN.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.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-bnI2zJeT.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-D267GQFF.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/judgment/store/index.js +158 -0
- package/lib/judgment/store/records.js +183 -24
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-gen.js +370 -21
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2803 -277
- package/lib/lane-gate.js +2 -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 +15 -1
- package/server/compose-mcp.js +96 -1
- package/server/mcp-tool-policy.js +1 -1
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.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-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.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
|
*/
|
|
@@ -14,6 +14,8 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { RecordsStore } from './records.js';
|
|
16
16
|
|
|
17
|
+
const EFFECTIVE_STORES = new WeakSet();
|
|
18
|
+
|
|
17
19
|
function configuredProvider(cwd) {
|
|
18
20
|
try {
|
|
19
21
|
const config = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf8'));
|
|
@@ -47,3 +49,159 @@ export function createJudgmentStore(cwd) {
|
|
|
47
49
|
throw new Error(`Unknown judgment provider '${provider}'. Valid ids: records, smartmemory.`);
|
|
48
50
|
}
|
|
49
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
|
+
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* (the feature.json precedent — NOT the gitignored vision store):
|
|
6
6
|
*
|
|
7
7
|
* positions/<slug>/r<N>.json append-only revision chain; highest N is current
|
|
8
|
+
* people/<slug>.json mutable person aggregate
|
|
9
|
+
* situation/<slug>.json mutable situation aggregate
|
|
10
|
+
* goal/v<N>.json append-only goal meaning chain
|
|
11
|
+
* goal/state.json mutable goal associations sidecar
|
|
8
12
|
* joints/<slug>.json one file per joint; state lives here
|
|
9
13
|
* predictions/<id>.json spawned by CONSTRUCT/commit events
|
|
10
14
|
* intents/<id>.json pending-intent records (intent-first transitions)
|
|
@@ -44,8 +48,6 @@ function readJson(path) {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
const REV_FILE_RE = /^r([1-9][0-9]*)\.json$/;
|
|
48
|
-
|
|
49
51
|
export class RecordsStore {
|
|
50
52
|
/** @param {string} cwd project root */
|
|
51
53
|
constructor(cwd) {
|
|
@@ -60,18 +62,77 @@ export class RecordsStore {
|
|
|
60
62
|
|
|
61
63
|
// ── positions ────────────────────────────────────────────────────────────
|
|
62
64
|
|
|
65
|
+
_chainNumbers({ dir, prefix }) {
|
|
66
|
+
if (!existsSync(dir)) return [];
|
|
67
|
+
const fileRe = new RegExp(`^${prefix}([1-9][0-9]*)\\.json$`);
|
|
68
|
+
return readdirSync(dir)
|
|
69
|
+
.map((file) => fileRe.exec(file))
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((match) => Number(match[1]))
|
|
72
|
+
.sort((a, b) => a - b);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_appendChainRecord(config, record) {
|
|
76
|
+
mkdirSync(config.dir, { recursive: true });
|
|
77
|
+
const numbers = this._chainNumbers(config);
|
|
78
|
+
const number = (numbers.length ? numbers[numbers.length - 1] : 0) + 1;
|
|
79
|
+
const path = join(config.dir, `${config.prefix}${number}.json`);
|
|
80
|
+
atomicWrite(
|
|
81
|
+
path,
|
|
82
|
+
JSON.stringify({ ...record, [config.numberField]: number }, null, 2) + '\n',
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
[config.numberField]: number,
|
|
86
|
+
ref: config.ref(number),
|
|
87
|
+
path,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_readChainRecord(config, number) {
|
|
92
|
+
return readJson(join(config.dir, `${config.prefix}${number}.json`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_readChain(config) {
|
|
96
|
+
return this._chainNumbers(config)
|
|
97
|
+
.map((number) => this._readChainRecord(config, number))
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_latestChainRecord(config) {
|
|
102
|
+
const numbers = this._chainNumbers(config);
|
|
103
|
+
if (numbers.length === 0) return null;
|
|
104
|
+
return this._readChainRecord(config, numbers[numbers.length - 1]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_replaceChainRecord(config, number, record) {
|
|
108
|
+
mkdirSync(config.dir, { recursive: true });
|
|
109
|
+
const path = join(config.dir, `${config.prefix}${number}.json`);
|
|
110
|
+
atomicWrite(
|
|
111
|
+
path,
|
|
112
|
+
JSON.stringify({ ...record, [config.numberField]: number }, null, 2) + '\n',
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
[config.numberField]: number,
|
|
116
|
+
ref: config.ref(number),
|
|
117
|
+
path,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
63
121
|
_positionDir(slug) {
|
|
64
122
|
return join(this.root, 'positions', slug);
|
|
65
123
|
}
|
|
66
124
|
|
|
125
|
+
_positionChain(slug) {
|
|
126
|
+
return {
|
|
127
|
+
dir: this._positionDir(slug),
|
|
128
|
+
prefix: 'r',
|
|
129
|
+
numberField: 'rev',
|
|
130
|
+
ref: (rev) => `${slug}#r${rev}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
67
134
|
_revNumbers(slug) {
|
|
68
|
-
|
|
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);
|
|
135
|
+
return this._chainNumbers(this._positionChain(slug));
|
|
75
136
|
}
|
|
76
137
|
|
|
77
138
|
/**
|
|
@@ -80,24 +141,16 @@ export class RecordsStore {
|
|
|
80
141
|
* @returns {{ rev: number, ref: string, path: string }}
|
|
81
142
|
*/
|
|
82
143
|
writePositionRevision(record) {
|
|
83
|
-
|
|
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 };
|
|
144
|
+
return this._appendChainRecord(this._positionChain(record.slug), record);
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
/** Full chain, ascending by rev. */
|
|
93
148
|
readPositionChain(slug) {
|
|
94
|
-
return this.
|
|
95
|
-
.map((n) => readJson(join(this._positionDir(slug), `r${n}.json`)))
|
|
96
|
-
.filter(Boolean);
|
|
149
|
+
return this._readChain(this._positionChain(slug));
|
|
97
150
|
}
|
|
98
151
|
|
|
99
152
|
readPositionRevision(slug, rev) {
|
|
100
|
-
return
|
|
153
|
+
return this._readChainRecord(this._positionChain(slug), rev);
|
|
101
154
|
}
|
|
102
155
|
|
|
103
156
|
listPositionSlugs() {
|
|
@@ -111,9 +164,7 @@ export class RecordsStore {
|
|
|
111
164
|
|
|
112
165
|
/** Latest revision of a chain, or null. */
|
|
113
166
|
latestPositionRevision(slug) {
|
|
114
|
-
|
|
115
|
-
if (revs.length === 0) return null;
|
|
116
|
-
return this.readPositionRevision(slug, revs[revs.length - 1]);
|
|
167
|
+
return this._latestChainRecord(this._positionChain(slug));
|
|
117
168
|
}
|
|
118
169
|
|
|
119
170
|
/**
|
|
@@ -134,6 +185,110 @@ export class RecordsStore {
|
|
|
134
185
|
return 'live';
|
|
135
186
|
}
|
|
136
187
|
|
|
188
|
+
// ── goal ─────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
_goalDir() {
|
|
191
|
+
return join(this.root, 'goal');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_goalChain() {
|
|
195
|
+
return {
|
|
196
|
+
dir: this._goalDir(),
|
|
197
|
+
prefix: 'v',
|
|
198
|
+
numberField: 'version',
|
|
199
|
+
ref: (version) => `goal:v${version}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
writeGoalVersion(record) {
|
|
204
|
+
return this._appendChainRecord(this._goalChain(), record);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
readGoalVersion(version) {
|
|
208
|
+
return this._readChainRecord(this._goalChain(), version);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
readGoalChain() {
|
|
212
|
+
return this._readChain(this._goalChain());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
latestGoalVersion() {
|
|
216
|
+
return this._latestChainRecord(this._goalChain());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
replaceGoalVersion(version, record) {
|
|
220
|
+
return this._replaceChainRecord(this._goalChain(), version, record);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_goalStatePath() {
|
|
224
|
+
return join(this._goalDir(), 'state.json');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
writeGoalState(record) {
|
|
228
|
+
mkdirSync(this._goalDir(), { recursive: true });
|
|
229
|
+
atomicWrite(this._goalStatePath(), JSON.stringify(record, null, 2) + '\n');
|
|
230
|
+
return { path: this._goalStatePath() };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
readGoalState() {
|
|
234
|
+
return readJson(this._goalStatePath());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── people ───────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
_personPath(slug) {
|
|
240
|
+
return join(this.root, 'people', `${slug}.json`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writePerson(record) {
|
|
244
|
+
mkdirSync(join(this.root, 'people'), { recursive: true });
|
|
245
|
+
atomicWrite(this._personPath(record.slug), JSON.stringify(record, null, 2) + '\n');
|
|
246
|
+
return { slug: record.slug };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
readPerson(slug) {
|
|
250
|
+
return readJson(this._personPath(slug));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
listPeople() {
|
|
254
|
+
const dir = join(this.root, 'people');
|
|
255
|
+
if (!existsSync(dir)) return [];
|
|
256
|
+
return readdirSync(dir)
|
|
257
|
+
.filter((file) => file.endsWith('.json'))
|
|
258
|
+
.sort()
|
|
259
|
+
.map((file) => readJson(join(dir, file)))
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── situation ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
_situationEntityPath(slug) {
|
|
266
|
+
return join(this.root, 'situation', `${slug}.json`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
writeSituationEntity(record) {
|
|
270
|
+
mkdirSync(join(this.root, 'situation'), { recursive: true });
|
|
271
|
+
atomicWrite(
|
|
272
|
+
this._situationEntityPath(record.slug),
|
|
273
|
+
JSON.stringify(record, null, 2) + '\n',
|
|
274
|
+
);
|
|
275
|
+
return { slug: record.slug };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
readSituationEntity(slug) {
|
|
279
|
+
return readJson(this._situationEntityPath(slug));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
listSituationEntities() {
|
|
283
|
+
const dir = join(this.root, 'situation');
|
|
284
|
+
if (!existsSync(dir)) return [];
|
|
285
|
+
return readdirSync(dir)
|
|
286
|
+
.filter((file) => file.endsWith('.json'))
|
|
287
|
+
.sort()
|
|
288
|
+
.map((file) => readJson(join(dir, file)))
|
|
289
|
+
.filter(Boolean);
|
|
290
|
+
}
|
|
291
|
+
|
|
137
292
|
// ── joints ───────────────────────────────────────────────────────────────
|
|
138
293
|
|
|
139
294
|
_jointPath(slug) {
|
|
@@ -241,6 +396,10 @@ export class RecordsStore {
|
|
|
241
396
|
}
|
|
242
397
|
|
|
243
398
|
clearIntent(id) {
|
|
244
|
-
try {
|
|
399
|
+
try {
|
|
400
|
+
unlinkSync(this._intentPath(id));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
403
|
+
}
|
|
245
404
|
}
|
|
246
405
|
}
|