@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.
Files changed (134) 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 +7 -0
  5. package/contracts/feature-json.schema.json +5 -0
  6. package/contracts/judgment-record.schema.json +425 -4
  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-DZf45Fbz.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
  14. package/dist/assets/channel-qVK_qn4E.js +1 -0
  15. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
  16. package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
  17. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
  19. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
  20. package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
  21. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
  22. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.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-BieYif4o.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-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
  41. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
  42. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.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-Ck7gpa5N.js → linear-AvSTWMqx.js} +1 -1
  46. package/dist/assets/min-QBM8H4xN.js +1 -0
  47. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.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-bnI2zJeT.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-D267GQFF.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/judgment/store/index.js +158 -0
  74. package/lib/judgment/store/records.js +183 -24
  75. package/lib/judgment-attest.js +259 -0
  76. package/lib/judgment-gen.js +370 -21
  77. package/lib/judgment-verify.js +153 -0
  78. package/lib/judgment-writer.js +2803 -277
  79. package/lib/lane-gate.js +2 -0
  80. package/lib/local-claude-connector.js +199 -54
  81. package/lib/mcp-enforcement.js +21 -35
  82. package/lib/result-normalizer.js +93 -15
  83. package/lib/review-normalize.js +4 -0
  84. package/lib/stratum-mcp-client.js +131 -6
  85. package/package.json +2 -2
  86. package/server/compose-mcp-tools.js +15 -1
  87. package/server/compose-mcp.js +96 -1
  88. package/server/mcp-tool-policy.js +1 -1
  89. package/dist/assets/App-BG3ngu8H.js +0 -896
  90. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  91. package/dist/assets/arc-_v4hR_uD.js +0 -1
  92. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  93. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  94. package/dist/assets/channel-B3U5wFAT.js +0 -1
  95. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  96. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  97. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  98. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  99. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  100. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  101. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  102. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  103. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  104. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  105. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  106. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  107. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  108. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  109. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  110. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  111. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  112. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  113. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  114. package/dist/assets/graph-BXPQrYYB.js +0 -331
  115. package/dist/assets/graph-C9eacEi8.js +0 -1
  116. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  117. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  118. package/dist/assets/katex-C5jXJg4s.js +0 -257
  119. package/dist/assets/layout-DEXfKzaS.js +0 -1
  120. package/dist/assets/map-Czzmt4hB.js +0 -1
  121. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  122. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  123. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  124. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  125. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  126. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  127. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  128. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  129. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  130. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  131. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  132. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  133. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  134. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.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
  */
@@ -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
- 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);
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
- 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 };
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._revNumbers(slug)
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 readJson(join(this._positionDir(slug), `r${rev}.json`));
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
- const revs = this._revNumbers(slug);
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 { unlinkSync(this._intentPath(id)); } catch { /* already cleared */ }
399
+ try {
400
+ unlinkSync(this._intentPath(id));
401
+ } catch (err) {
402
+ if (err?.code !== 'ENOENT') throw err;
403
+ }
245
404
  }
246
405
  }