@smartmemory/compose 0.3.7 → 0.4.0
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 +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +57 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -23
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +396 -199
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +271 -36
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +109 -23
- package/server/compose-mcp.js +88 -882
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +34 -2
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-inventory.js — COMP-COVERAGE-GATE slice 1.
|
|
3
|
+
*
|
|
4
|
+
* The single derived answer to "which MCP tools change state, and what canon do
|
|
5
|
+
* they write". Pure, no I/O (shape template: server/mcp-tool-policy.js).
|
|
6
|
+
*
|
|
7
|
+
* WHY THIS EXISTS. Compose kept three hand-maintained lists that each partially
|
|
8
|
+
* described the mutating surface — canon-registry's TOOLS_FOR_* /
|
|
9
|
+
* JUDGMENT_WRITE_TOOLS, and mcp-tool-policy's IMPLEMENTER_DENY / REVIEWER_ALLOW
|
|
10
|
+
* — and NONE of them was the inventory. Nothing enumerated every mutating tool,
|
|
11
|
+
* and nothing cross-checked the three against the tool definitions themselves.
|
|
12
|
+
* A tool added to server/compose-mcp.js was enforced only where an author
|
|
13
|
+
* remembered to name it. Same shape as COMP-COMPLETION-GATE, where guard
|
|
14
|
+
* coverage turned out to be 0/321 — discovered by audit, not by a check.
|
|
15
|
+
*
|
|
16
|
+
* The fix is to put the declaration ON the tool definition (`effect`, `writes`)
|
|
17
|
+
* and DERIVE the sets from it. A side list is a fourth thing to forget; a
|
|
18
|
+
* required field on the definition cannot be added without the author seeing it.
|
|
19
|
+
*
|
|
20
|
+
* FAIL-CLOSED HERE, FAIL-OPEN AT RUNTIME. A tool with no `effect` is reported as
|
|
21
|
+
* `undeclared` and fails validate_project. That is safe because this module runs
|
|
22
|
+
* at validate time and never in a request path — unlike canon-guard.js, which
|
|
23
|
+
* fails open so a malformed input can never wedge a session.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Valid `effect` values on a tool definition. */
|
|
27
|
+
export const EFFECTS = /** @type {const} */ (['read', 'mutating', 'setup']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Canon path ids a `writes` entry may name — the `id` field of each
|
|
31
|
+
* lib/canon-registry.js REGISTRY entry. Kept as a literal rather than imported
|
|
32
|
+
* so this module stays dependency-free and pure; `checkAuthorizationCoverage`
|
|
33
|
+
* (slice 2) validates `writes` against the live registry, which is where a
|
|
34
|
+
* drifted id must be caught. This set is only for the shape check below.
|
|
35
|
+
*/
|
|
36
|
+
export const CANON_IDS = new Set([
|
|
37
|
+
'roadmap', 'changelog', 'feature-json', 'judgment',
|
|
38
|
+
'override-ledger', 'override-attest', 'override-grants',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Partition tool definitions by declared effect.
|
|
43
|
+
*
|
|
44
|
+
* @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
|
|
45
|
+
* @returns {{
|
|
46
|
+
* read: string[], mutating: string[], setup: string[],
|
|
47
|
+
* undeclared: Array<{ name: string, reason: string }>,
|
|
48
|
+
* writesByTool: Record<string, string[]>,
|
|
49
|
+
* }}
|
|
50
|
+
*/
|
|
51
|
+
export function loadToolInventory(toolDefs) {
|
|
52
|
+
const read = [];
|
|
53
|
+
const mutating = [];
|
|
54
|
+
const setup = [];
|
|
55
|
+
const undeclared = [];
|
|
56
|
+
/** @type {Record<string, string[]>} */
|
|
57
|
+
const writesByTool = {};
|
|
58
|
+
|
|
59
|
+
if (!Array.isArray(toolDefs)) return { read, mutating, setup, undeclared, writesByTool };
|
|
60
|
+
|
|
61
|
+
for (const def of toolDefs) {
|
|
62
|
+
const name = def && typeof def.name === 'string' ? def.name : null;
|
|
63
|
+
if (!name) {
|
|
64
|
+
undeclared.push({ name: '<unnamed>', reason: 'tool definition has no name' });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const effect = def.effect;
|
|
69
|
+
if (!EFFECTS.includes(effect)) {
|
|
70
|
+
undeclared.push({
|
|
71
|
+
name,
|
|
72
|
+
reason: effect === undefined
|
|
73
|
+
? 'no `effect` field'
|
|
74
|
+
: `unknown effect '${effect}' (expected ${EFFECTS.join('|')})`,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (effect === 'read') { read.push(name); continue; }
|
|
80
|
+
if (effect === 'setup') { setup.push(name); continue; }
|
|
81
|
+
|
|
82
|
+
// effect === 'mutating' — `writes` is required, `[]` being a real answer
|
|
83
|
+
// ("mutates state, but no CANON path"). Distinguishing "declared empty"
|
|
84
|
+
// from "forgot to declare" is the entire point of requiring the field.
|
|
85
|
+
if (!Array.isArray(def.writes)) {
|
|
86
|
+
undeclared.push({ name, reason: 'mutating tool has no `writes` array (use [] for non-canon writes)' });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const bad = def.writes.filter((w) => !CANON_IDS.has(w));
|
|
90
|
+
if (bad.length > 0) {
|
|
91
|
+
undeclared.push({ name, reason: `\`writes\` names unknown canon id(s): ${bad.join(', ')}` });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
mutating.push(name);
|
|
95
|
+
writesByTool[name] = [...def.writes];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { read, mutating, setup, undeclared, writesByTool };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The derived mutating set. Deliberately a function of the definitions rather
|
|
103
|
+
* than a literal — a literal is exactly the drift this feature exists to stop.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
|
|
106
|
+
* @returns {Set<string>}
|
|
107
|
+
*/
|
|
108
|
+
export function mutatingTools(toolDefs) {
|
|
109
|
+
return new Set(loadToolInventory(toolDefs).mutating);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Tools that write a given canon path id, derived from `writes`.
|
|
114
|
+
*
|
|
115
|
+
* @param {Array<{name?: string, effect?: string, writes?: string[]}>} toolDefs
|
|
116
|
+
* @param {string} canonId
|
|
117
|
+
* @returns {string[]}
|
|
118
|
+
*/
|
|
119
|
+
export function toolsWritingCanon(toolDefs, canonId) {
|
|
120
|
+
const { writesByTool } = loadToolInventory(toolDefs);
|
|
121
|
+
return Object.keys(writesByTool).filter((t) => writesByTool[t].includes(canonId)).sort();
|
|
122
|
+
}
|
package/lib/version-check.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* on any failure (network, parse, etc.) so it never breaks `compose doctor`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
10
10
|
import { join, dirname } from 'path'
|
|
11
11
|
import { homedir } from 'os'
|
|
12
12
|
|
|
@@ -14,38 +14,48 @@ const PACKAGE_NAME = '@smartmemory/compose'
|
|
|
14
14
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24h
|
|
15
15
|
const FETCH_TIMEOUT_MS = 3000
|
|
16
16
|
|
|
17
|
-
function cachePath() {
|
|
18
|
-
return join(homedir(), '.compose', 'version-cache.json')
|
|
17
|
+
export function cachePath() {
|
|
18
|
+
return process.env.COMPOSE_VERSION_CACHE || join(homedir(), '.compose', 'version-cache.json')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function
|
|
21
|
+
function readCacheDoc(file) {
|
|
22
22
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'))
|
|
24
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
|
25
|
+
// Legacy cache is exactly { fetchedAt: <number>, latest: <string> }.
|
|
26
|
+
// A package named `latest` is valid and would otherwise be mistaken for that shape.
|
|
27
|
+
if (typeof parsed.fetchedAt === 'number' && typeof parsed.latest === 'string') return {}
|
|
27
28
|
return parsed
|
|
28
29
|
} catch {
|
|
29
|
-
return
|
|
30
|
+
return {}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function
|
|
34
|
+
function readCache(pkg, file) {
|
|
35
|
+
const entry = readCacheDoc(file)[pkg]
|
|
36
|
+
if (typeof entry?.fetchedAt !== 'number' || typeof entry?.latest !== 'string') return null
|
|
37
|
+
if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) return null
|
|
38
|
+
return entry
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeCache(pkg, latest, file) {
|
|
34
42
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
44
|
+
const doc = readCacheDoc(file)
|
|
45
|
+
doc[pkg] = { fetchedAt: Date.now(), latest }
|
|
46
|
+
writeFileSync(file, JSON.stringify(doc, null, 2))
|
|
38
47
|
} catch {
|
|
39
48
|
// best-effort cache; ignore failures
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
async function fetchLatest() {
|
|
52
|
+
async function fetchLatest(pkg) {
|
|
53
|
+
if (process.env.COMPOSE_VERSION_CHECK_OFFLINE === '1') return null
|
|
44
54
|
if (typeof fetch !== 'function') return null
|
|
45
55
|
const controller = new AbortController()
|
|
46
56
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
47
57
|
try {
|
|
48
|
-
const url = `https://registry.npmjs.org/${
|
|
58
|
+
const url = `https://registry.npmjs.org/${pkg}/latest`
|
|
49
59
|
const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
50
60
|
if (!res.ok) return null
|
|
51
61
|
const data = await res.json()
|
|
@@ -89,11 +99,12 @@ export function compareVersions(a, b) {
|
|
|
89
99
|
* behind: true if current < latest, false otherwise.
|
|
90
100
|
* source: 'cache' | 'network'
|
|
91
101
|
*/
|
|
92
|
-
export async function
|
|
102
|
+
export async function checkPackageVersion(pkg, currentVersion, opts) {
|
|
103
|
+
const { force = false, fetchImpl = fetchLatest, cacheFile = cachePath(), cacheOnly = false } = opts ?? {}
|
|
93
104
|
if (!currentVersion) return null
|
|
94
105
|
|
|
95
106
|
if (!force) {
|
|
96
|
-
const cached = readCache()
|
|
107
|
+
const cached = readCache(pkg, cacheFile)
|
|
97
108
|
if (cached) {
|
|
98
109
|
const cmp = compareVersions(currentVersion, cached.latest)
|
|
99
110
|
if (cmp === null) return null
|
|
@@ -101,10 +112,71 @@ export async function checkLatestVersion(currentVersion, { force = false } = {})
|
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
// cacheOnly callers accept a miss rather than pay for the network. Used by the
|
|
116
|
+
// CLI nudge for non-interactive callers, where a courtesy line is not worth a
|
|
117
|
+
// request — and where concurrent spawns once left the fetch unsettled.
|
|
118
|
+
if (cacheOnly) return null
|
|
119
|
+
|
|
120
|
+
let latest = null
|
|
121
|
+
try {
|
|
122
|
+
latest = await fetchImpl(pkg)
|
|
123
|
+
} catch {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
105
126
|
if (!latest) return null
|
|
106
|
-
writeCache(latest)
|
|
127
|
+
writeCache(pkg, latest, cacheFile)
|
|
107
128
|
const cmp = compareVersions(currentVersion, latest)
|
|
108
129
|
if (cmp === null) return null
|
|
109
130
|
return { current: currentVersion, latest, behind: cmp < 0, source: 'network' }
|
|
110
131
|
}
|
|
132
|
+
|
|
133
|
+
/** Compose's own check. Shape and behavior unchanged — `compose doctor` and its
|
|
134
|
+
* --json consumers depend on this exact object. */
|
|
135
|
+
export async function checkLatestVersion(currentVersion, opts = {}) {
|
|
136
|
+
return checkPackageVersion(PACKAGE_NAME, currentVersion, opts ?? {})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The version stratum ACTUALLY resolved to, read from node_modules.
|
|
140
|
+
*
|
|
141
|
+
* Deliberately not the range declared in compose's package.json. Compose 0.3.7
|
|
142
|
+
* moved that declaration from an exact pin to `^0.3.3`, and npm does not
|
|
143
|
+
* re-resolve a caret unless something triggers an install — so the declared
|
|
144
|
+
* range and the installed version drift apart, and the installed one is the
|
|
145
|
+
* only one that tells the user anything true.
|
|
146
|
+
*
|
|
147
|
+
* Absent stratum is a normal state (not installed, capabilities.stratum off),
|
|
148
|
+
* not an error. */
|
|
149
|
+
export function resolveStratumVersion(packageRoot) {
|
|
150
|
+
try {
|
|
151
|
+
const manifest = join(packageRoot, 'node_modules', '@smartmemory', 'stratum', 'package.json')
|
|
152
|
+
const version = JSON.parse(readFileSync(manifest, 'utf-8')).version
|
|
153
|
+
return typeof version === 'string' ? version : null
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Zero or one lines. Pure: no network, no filesystem, no clock — so every
|
|
160
|
+
* branch is decidable in a unit test.
|
|
161
|
+
*
|
|
162
|
+
* Silence is the default. A nudge that prints on every run gets filtered out by
|
|
163
|
+
* the reader, at which point it has negative value. */
|
|
164
|
+
export function formatDriftNudge(arg = {}) {
|
|
165
|
+
if (!arg || typeof arg !== 'object') return []
|
|
166
|
+
const { compose = null, stratum = null } = arg
|
|
167
|
+
const composeRecord = compose && typeof compose === 'object' ? compose : null
|
|
168
|
+
const stratumRecord = stratum && typeof stratum === 'object' ? stratum : null
|
|
169
|
+
const valid = (record) =>
|
|
170
|
+
record &&
|
|
171
|
+
record.behind === true &&
|
|
172
|
+
typeof record.current === 'string' &&
|
|
173
|
+
record.current !== '' &&
|
|
174
|
+
typeof record.latest === 'string' &&
|
|
175
|
+
record.latest !== ''
|
|
176
|
+
|
|
177
|
+
const parts = []
|
|
178
|
+
if (valid(composeRecord)) parts.push(`compose ${compose.current} → ${compose.latest}`)
|
|
179
|
+
if (valid(stratumRecord)) parts.push(`stratum ${stratum.current} → ${stratum.latest}`)
|
|
180
|
+
if (parts.length === 0) return []
|
|
181
|
+
return [`⚠ update available: ${parts.join(', ')} — run: compose update`]
|
|
182
|
+
}
|
package/lib/vision-writer.js
CHANGED
|
@@ -401,12 +401,99 @@ export class VisionWriter {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
async updateItemStatus(itemId, status) {
|
|
404
|
-
|
|
404
|
+
const up = await this._serverAvailable();
|
|
405
|
+
// COMP-COMPLETION-GATE slice 3 (AC-16, path 14): `complete` is not a status
|
|
406
|
+
// this general writer sets on a managed build-mode feature item, in EITHER
|
|
407
|
+
// transport. The legitimate write is `completeItem` — the self-verifying
|
|
408
|
+
// seam of §2.3b, which re-reads canonical state before writing. Fix/plan
|
|
409
|
+
// items, UI items with no lifecycle, and the `compose new` kickoff item
|
|
410
|
+
// (build mode, no feature.json) are not feature completions and keep this
|
|
411
|
+
// path (§2.3d). In REST mode the server's PATCH refuses too (AC-10); the
|
|
412
|
+
// pre-check here makes the refusal the writer's own, with the writer's message.
|
|
413
|
+
if (status === 'complete') {
|
|
414
|
+
const item = up ? await this._restGetItem(itemId) : this._load().items.find(i => i.id === itemId);
|
|
415
|
+
const { isManagedBuildItem } = await import('../server/completion-projection.js');
|
|
416
|
+
if (item && isManagedBuildItem(item, this._workspaceRoot())) {
|
|
417
|
+
const e = new Error(
|
|
418
|
+
`VisionWriter.updateItemStatus: refusing to mark ${itemId} (${item.lifecycle.featureCode}) ` +
|
|
419
|
+
`complete — a managed build-mode feature completes through the completion gate, which ` +
|
|
420
|
+
`projects it via completeItem() after verifying the commit, tests and guard ledger.`,
|
|
421
|
+
);
|
|
422
|
+
e.code = 'COMPLETE_VIA_GATE_ONLY';
|
|
423
|
+
throw e;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (up) {
|
|
405
427
|
return this._restUpdateItemStatus(itemId, status);
|
|
406
428
|
}
|
|
407
429
|
return this._directUpdateItemStatus(itemId, status);
|
|
408
430
|
}
|
|
409
431
|
|
|
432
|
+
/**
|
|
433
|
+
* COMP-COMPLETION-GATE slice 3 (§2.3b, AC-4b): the ONLY path that writes
|
|
434
|
+
* `status: 'complete'` on a managed build-mode feature item. It carries no
|
|
435
|
+
* authority: in REST mode the server re-reads feature.json + the guard ledger
|
|
436
|
+
* before writing (`POST …/completion-projection`); in direct mode the same
|
|
437
|
+
* predicate runs here. Either way a completion that canonical state does not
|
|
438
|
+
* already record is refused.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} itemId
|
|
441
|
+
* @param {object} evidence
|
|
442
|
+
* @param {string} evidence.featureCode
|
|
443
|
+
* @param {string} [evidence.cwd] workspace root; derived from dataDir when omitted
|
|
444
|
+
* @param {string} [evidence.commitSha]
|
|
445
|
+
* @param {string} [evidence.ledgerRef]
|
|
446
|
+
* @returns {Promise<{ok:true, itemId:string, verified_by:string}>}
|
|
447
|
+
*/
|
|
448
|
+
async completeItem(itemId, evidence = {}) {
|
|
449
|
+
if (!evidence.featureCode) throw new Error('VisionWriter.completeItem: evidence.featureCode is required');
|
|
450
|
+
if (await this._serverAvailable()) {
|
|
451
|
+
const r = await this._fetch(`/api/vision/items/${itemId}/completion-projection`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
featureCode: evidence.featureCode,
|
|
455
|
+
commitSha: evidence.commitSha ?? null,
|
|
456
|
+
ledgerRef: evidence.ledgerRef ?? null,
|
|
457
|
+
}),
|
|
458
|
+
});
|
|
459
|
+
return { ok: true, itemId, verified_by: r.verified_by, transport: 'rest' };
|
|
460
|
+
}
|
|
461
|
+
return this._directCompleteItem(itemId, { ...evidence, cwd: evidence.cwd || this._workspaceRoot() });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** The workspace root this writer's dataDir belongs to (`<root>/.compose/data`). */
|
|
465
|
+
_workspaceRoot() {
|
|
466
|
+
return path.resolve(this._dataDir, '..', '..');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async _restGetItem(itemId) {
|
|
470
|
+
try {
|
|
471
|
+
return await this._fetch(`/api/vision/items/${itemId}`);
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async _directCompleteItem(itemId, evidence) {
|
|
478
|
+
const state = this._load();
|
|
479
|
+
const item = state.items.find(i => i.id === itemId);
|
|
480
|
+
if (!item) throw new Error(`VisionWriter.completeItem: item not found: ${itemId}`);
|
|
481
|
+
const { verifiedCompleteProjection, projectionStamp } = await import('../server/completion-projection.js');
|
|
482
|
+
const v = await verifiedCompleteProjection({
|
|
483
|
+
item, featureCode: evidence.featureCode, cwd: evidence.cwd, consultGuard: true,
|
|
484
|
+
});
|
|
485
|
+
if (!v.ok) {
|
|
486
|
+
const e = new Error(`VisionWriter.completeItem: projection refused — ${v.reasons.join('; ')}`);
|
|
487
|
+
e.code = 'PROJECTION_REFUSED';
|
|
488
|
+
e.reasons = v.reasons;
|
|
489
|
+
throw e;
|
|
490
|
+
}
|
|
491
|
+
item.status = 'complete';
|
|
492
|
+
item.completion_projection = projectionStamp(v, { ...evidence, source: 'direct' });
|
|
493
|
+
this._atomicWrite(state);
|
|
494
|
+
return { ok: true, itemId, verified_by: v.verified_by, transport: 'direct' };
|
|
495
|
+
}
|
|
496
|
+
|
|
410
497
|
async updateItemPhase(itemId, stepId) {
|
|
411
498
|
if (await this._serverAvailable()) {
|
|
412
499
|
return this._restUpdateItemPhase(itemId, stepId);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Structured AI dev pipeline
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Structured AI dev pipeline: your agent writes the code, Compose makes it prove it. Gated design decisions, enforced postconditions, and independent review from goal to shipped code.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"dev:client": "vite",
|
|
21
21
|
"build": "vite build",
|
|
22
22
|
"preview": "vite preview",
|
|
23
|
-
"test": "node --import ./test/suppress-expected-drift.js --test --test-timeout=
|
|
23
|
+
"test": "node --import ./test/suppress-expected-drift.js --test --test-timeout=300000 test/*.test.js test/comp-obs-branch/*.test.js test/integration/*.test.js test/golden/*.test.js && npm run test:ui && npm run test:tracker",
|
|
24
24
|
"test:ui": "vitest run",
|
|
25
25
|
"test:tracker": "vitest run --config vitest.tracker.config.js",
|
|
26
|
-
"test:integration": "node --test --test-timeout=
|
|
27
|
-
"test:wave-6": "node --test --test-timeout=
|
|
26
|
+
"test:integration": "node --test --test-timeout=300000 test/integration/*.test.js",
|
|
27
|
+
"test:wave-6": "node --test --test-timeout=300000 test/wave-6-integration.test.js test/wave-6-contract-compliance.test.js",
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
86
86
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
87
87
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
88
|
-
"@smartmemory/
|
|
88
|
+
"@smartmemory/sdk-js": "^1.4.60",
|
|
89
|
+
"@smartmemory/stratum": "^0.4.5",
|
|
89
90
|
"@tanstack/react-virtual": "^3.13.23",
|
|
90
91
|
"ajv": "^8.18.0",
|
|
91
92
|
"ajv-formats": "^3.0.1",
|