@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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/factory.js — configured fluid-store provider selection.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `lib/tracker/factory.js` in config handling (absent means default,
|
|
5
|
+
* malformed means fail loud) and DELIBERATELY DIVERGES from it in one respect:
|
|
6
|
+
* there is no `withFallback` proxy here.
|
|
7
|
+
*
|
|
8
|
+
* The tracker seam wraps its active provider so that an entity the provider
|
|
9
|
+
* cannot store falls through to the local one. That is right for the tracker,
|
|
10
|
+
* where the fallback is STORAGE and the substituted answer is equally true.
|
|
11
|
+
* It is wrong here. `PROVIDER-SEAM` forbids it: "a provider without a
|
|
12
|
+
* capability lacks it visibly; nothing fakes it." A fallback that answered
|
|
13
|
+
* `recall()` or `challenge()` from the floor would return a real-looking result
|
|
14
|
+
* produced by machinery that does not exist, which is worse than an error —
|
|
15
|
+
* the caller cannot tell the difference, and neither can the user.
|
|
16
|
+
*
|
|
17
|
+
* So capability absence propagates as `FluidCapabilityUnavailable` from
|
|
18
|
+
* `FluidProvider.require()`, and surfaces render it as a funnel ("challenge:
|
|
19
|
+
* connect SmartMemory") rather than an empty state.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { getSmartmemoryConfig } from '../smartmemory-config.js';
|
|
26
|
+
import { FluidConfigError, MUTATION_SCOPE, mutationScopeAtLeast } from './provider.js';
|
|
27
|
+
import { LocalFluidProvider } from './local-provider.js';
|
|
28
|
+
import { SmartMemoryFluidProvider } from './smartmemory-provider.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read `.compose/compose.json` → `fluid`.
|
|
32
|
+
* Absent file or absent key → local floor. That is a valid, supported
|
|
33
|
+
* configuration (the zero-install default), not a misconfiguration.
|
|
34
|
+
*/
|
|
35
|
+
function loadFluidConfig(cwd) {
|
|
36
|
+
const p = join(cwd, '.compose/compose.json');
|
|
37
|
+
if (!existsSync(p)) return { provider: 'local' };
|
|
38
|
+
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// The file EXISTS but is malformed. Falling back silently here would mask a
|
|
44
|
+
// typo in the user's config and quietly downgrade them to the floor —
|
|
45
|
+
// meaning their configured semantic capabilities would vanish with no
|
|
46
|
+
// signal. Fail loud.
|
|
47
|
+
throw new FluidConfigError(
|
|
48
|
+
`compose: fluid config at ${p} contains invalid JSON — ${e.message}`,
|
|
49
|
+
{ path: p }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Valid JSON is not a valid config. `[]`, `"smartmemory"` and `42` all parse,
|
|
54
|
+
// and reading `.fluid` off them yields undefined — which would silently select
|
|
55
|
+
// the floor, exactly the quiet downgrade this function exists to prevent.
|
|
56
|
+
// `null` would throw a bare TypeError instead of a config error.
|
|
57
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
58
|
+
throw new FluidConfigError(
|
|
59
|
+
`compose: fluid config at ${p} must be a JSON object ` +
|
|
60
|
+
`(got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'array' : typeof parsed})`,
|
|
61
|
+
{ path: p }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fluid = parsed.fluid;
|
|
66
|
+
if (fluid === undefined || fluid === null) return { provider: 'local' };
|
|
67
|
+
if (typeof fluid !== 'object' || Array.isArray(fluid)) {
|
|
68
|
+
throw new FluidConfigError(
|
|
69
|
+
`compose: fluid config at ${p} has a "fluid" key but it is not an object ` +
|
|
70
|
+
`(got ${Array.isArray(fluid) ? 'array' : typeof fluid})`,
|
|
71
|
+
{ path: p }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return fluid;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Construct the configured provider.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} cwd project root
|
|
81
|
+
* @param {object} [opts] passed through to the provider's init (the floor takes
|
|
82
|
+
* `recordsRoot` to relocate its tracked record tree)
|
|
83
|
+
* @returns {Promise<import('./provider.js').FluidProvider>}
|
|
84
|
+
*/
|
|
85
|
+
export async function fluidProviderFor(cwd, opts = {}) {
|
|
86
|
+
return warnIfUnsafelyShared(await construct(cwd, opts));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Warn when a provider's store is reachable from more machines than its
|
|
91
|
+
* serialization is.
|
|
92
|
+
*
|
|
93
|
+
* DERIVED FROM THE PROVIDER'S OWN DECLARATIONS, never hardcoded per provider.
|
|
94
|
+
* A hardcoded warning has to be remembered by whoever adds the third provider —
|
|
95
|
+
* which is the same "the second implementation looked complete" failure this
|
|
96
|
+
* whole feature exists to close — and it has to be remembered AGAIN, in the
|
|
97
|
+
* other direction, on the day the gap is fixed, or it keeps crying wolf.
|
|
98
|
+
*
|
|
99
|
+
* Warns rather than refuses: storage and recall work, COMP-FOH shipped them
|
|
100
|
+
* deliberately, and refusing would break a configuration that is fine for
|
|
101
|
+
* everything except concurrent writes. Silence is the only wrong option, because
|
|
102
|
+
* the failure is invisible until it has cost an idea.
|
|
103
|
+
*/
|
|
104
|
+
function warnIfUnsafelyShared(provider) {
|
|
105
|
+
if (provider.isShared() && !mutationScopeAtLeast(provider.mutationScope(), MUTATION_SCOPE.CLUSTER)) {
|
|
106
|
+
console.warn(
|
|
107
|
+
`compose: fluid provider "${provider.name()}" is shared across machines but serializes `
|
|
108
|
+
+ `mutation only at "${provider.mutationScope()}" scope, so concurrent writes can allocate `
|
|
109
|
+
+ `the same handle and lose records. Prefer the local provider for the ideabox until its `
|
|
110
|
+
+ `store offers a cross-machine reservation primitive (SmartMemory SVC-LEASE-1).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return provider;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function construct(cwd, opts = {}) {
|
|
117
|
+
const cfg = loadFluidConfig(cwd);
|
|
118
|
+
const name = cfg.provider ?? 'local';
|
|
119
|
+
|
|
120
|
+
if (name === 'local') {
|
|
121
|
+
return new LocalFluidProvider().init(cwd, { ...cfg.local, ...opts });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (name === 'smartmemory') {
|
|
125
|
+
// The warning that used to live here is gone, not deleted: it is now
|
|
126
|
+
// `warnIfUnsafelyShared`, derived from this provider's own
|
|
127
|
+
// `isShared()`/`mutationScope()` rather than hardcoded to its name
|
|
128
|
+
// (COMP-FLUID-SEAM-GUARANTEES). BOTH gaps it named are now closed — the
|
|
129
|
+
// restartable import first, then cross-machine serialization once
|
|
130
|
+
// SmartMemory shipped SVC-ALLOC-1 and SVC-LEASE-1 (COMP-FLUID-SEAM-GUARANTEES) — so this
|
|
131
|
+
// provider declares CLUSTER and the warning no longer fires for it. That is
|
|
132
|
+
// exactly the "remember it AGAIN, in the other direction" the check below
|
|
133
|
+
// was written to make automatic: nothing here had to be edited to stop the
|
|
134
|
+
// warning, the declaration moving was enough.
|
|
135
|
+
|
|
136
|
+
// The endpoint and credential come from the EXISTING top-level
|
|
137
|
+
// `smartmemory` block, shared with the shipped kitchen pipeline, so there
|
|
138
|
+
// is one source of truth for where SmartMemory lives. Only `workspaceId`
|
|
139
|
+
// is new, and it lives under `fluid.smartmemory` because it scopes fluid
|
|
140
|
+
// records specifically.
|
|
141
|
+
//
|
|
142
|
+
// A missing block is a config error, not a downgrade to the floor: a user
|
|
143
|
+
// who configured SmartMemory did so to get capabilities the floor does not
|
|
144
|
+
// have, and starting up on the floor instead would present an
|
|
145
|
+
// intelligence-free system as a working one.
|
|
146
|
+
const sm = getSmartmemoryConfig(cwd);
|
|
147
|
+
if (!sm || Object.keys(sm).length === 0) {
|
|
148
|
+
throw new FluidConfigError(
|
|
149
|
+
'compose: fluid provider "smartmemory" requires a top-level "smartmemory" config ' +
|
|
150
|
+
'block in .compose/compose.json (baseUrl + apiKeyEnv). It is shared with the ' +
|
|
151
|
+
'SmartMemory ingest pipeline rather than duplicated under "fluid".',
|
|
152
|
+
{ provider: name, setting: 'smartmemory' }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return new SmartMemoryFluidProvider().init(cwd, {
|
|
156
|
+
baseUrl: sm.baseUrl,
|
|
157
|
+
apiKeyEnv: sm.apiKeyEnv,
|
|
158
|
+
timeoutMs: sm.timeoutMs,
|
|
159
|
+
...cfg.smartmemory,
|
|
160
|
+
...opts,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new FluidConfigError(`compose: unknown fluid provider "${name}"`, { provider: name });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { loadFluidConfig };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/ideabox-dates.js — the date vocabulary of the markdown boundary.
|
|
3
|
+
*
|
|
4
|
+
* COMP-PLAN-IDEA-UNIFY S3b-1.
|
|
5
|
+
*
|
|
6
|
+
* The record contract types every event date as `format: "date-time"`
|
|
7
|
+
* (`contracts/fluid-record.schema.json` — `killed.at`, `discussion[].at`), but
|
|
8
|
+
* the ideabox markdown has only ever carried a bare `YYYY-MM-DD`: the
|
|
9
|
+
* discussion grammar is `- [YYYY-MM-DD] author: text`
|
|
10
|
+
* (`lib/ideabox.js:53`) and `killIdea` stamps `…toISOString().slice(0, 10)`
|
|
11
|
+
* (`lib/ideabox.js:546`).
|
|
12
|
+
*
|
|
13
|
+
* Two directions, and BOTH were wrong before this module existed:
|
|
14
|
+
*
|
|
15
|
+
* import markdown date → record date-time (widen)
|
|
16
|
+
* render record date-time → markdown date (narrow)
|
|
17
|
+
*
|
|
18
|
+
* The importer passed the bare date straight into the contract, so importing
|
|
19
|
+
* any idea carrying a discussion entry or a kill date threw
|
|
20
|
+
* `must match format "date-time"`. The renderer emitted the full ISO timestamp
|
|
21
|
+
* straight into the markdown, where `DISCUSSION_ENTRY_RE` cannot match it — so
|
|
22
|
+
* a provider-written discussion entry degraded to an unparsed extra line and
|
|
23
|
+
* was silently lost on the next read.
|
|
24
|
+
*
|
|
25
|
+
* Neither defect was caught, because no idea on disk had ever carried a
|
|
26
|
+
* discussion entry and the Killed Ideas section was empty. Both paths were
|
|
27
|
+
* dead code with passing tests over them.
|
|
28
|
+
*
|
|
29
|
+
* They live together in one module deliberately: the two conversions are a
|
|
30
|
+
* single round-trip contract, and their drifting apart is precisely the bug.
|
|
31
|
+
* Splitting them across the importer and the renderer is what let it happen.
|
|
32
|
+
*
|
|
33
|
+
* Precision is intentionally asymmetric. A record keeps the full timestamp; the
|
|
34
|
+
* projection is a view and shows the day. Rendering a date-time into a file
|
|
35
|
+
* whose grammar is a date does not preserve information, it corrupts the line.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** A bare calendar date, the only date form the ideabox markdown can carry. */
|
|
39
|
+
const MARKDOWN_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
40
|
+
|
|
41
|
+
const EPOCH = new Date(0).toISOString();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Widen a markdown date to a contract-valid `date-time`.
|
|
45
|
+
*
|
|
46
|
+
* A value that already carries a time is passed through untouched, so this is
|
|
47
|
+
* safe to apply to input of mixed provenance (an imported entry and a
|
|
48
|
+
* provider-written one can sit in the same array).
|
|
49
|
+
*
|
|
50
|
+
* @param {string|null|undefined} value a `YYYY-MM-DD`, a full ISO timestamp, or nothing
|
|
51
|
+
* @returns {string} an ISO 8601 date-time
|
|
52
|
+
*/
|
|
53
|
+
export function toRecordTimestamp(value) {
|
|
54
|
+
if (!value) return EPOCH;
|
|
55
|
+
if (MARKDOWN_DATE_RE.test(value)) return `${value}T00:00:00.000Z`;
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Narrow a contract `date-time` to the markdown's `YYYY-MM-DD`.
|
|
61
|
+
*
|
|
62
|
+
* Anything unparseable is returned unchanged rather than coerced: emitting a
|
|
63
|
+
* wrong-but-well-formed date would be worse than emitting the raw value, which
|
|
64
|
+
* is at least visibly odd.
|
|
65
|
+
*
|
|
66
|
+
* @param {string|null|undefined} value an ISO 8601 date-time
|
|
67
|
+
* @returns {string} a `YYYY-MM-DD`
|
|
68
|
+
*/
|
|
69
|
+
export function toMarkdownDate(value) {
|
|
70
|
+
if (typeof value !== 'string') return '';
|
|
71
|
+
const head = value.slice(0, 10);
|
|
72
|
+
return MARKDOWN_DATE_RE.test(head) ? head : value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/ideabox-migrate.js — the first-use gate between a markdown ideabox
|
|
3
|
+
* and the record store.
|
|
4
|
+
*
|
|
5
|
+
* COMP-PLAN-IDEA-UNIFY S3b-1 (F2).
|
|
6
|
+
*
|
|
7
|
+
* THE FAILURE THIS EXISTS TO PREVENT
|
|
8
|
+
* ----------------------------------
|
|
9
|
+
* `@smartmemory/compose` is published. Other projects have their own populated
|
|
10
|
+
* `docs/product/ideabox.md` and no fluid records. The cutover makes records
|
|
11
|
+
* canon and the markdown a projection of them — so without this gate, the first
|
|
12
|
+
* `compose ideabox add` in an upgraded project allocates IDEA-1 against an empty
|
|
13
|
+
* store and the projection replaces that project's entire ideabox with the one
|
|
14
|
+
* idea they just typed. Their ideas would survive only in their git history.
|
|
15
|
+
*
|
|
16
|
+
* The blueprint missed this by treating the import as a one-time operation on
|
|
17
|
+
* THIS repository. It is not: it is an upgrade path, and it runs once per
|
|
18
|
+
* installation, on whatever that installation happens to have.
|
|
19
|
+
*
|
|
20
|
+
* WHY IT REFUSES RATHER THAN REPAIRS
|
|
21
|
+
* ----------------------------------
|
|
22
|
+
* The dangerous state is not "no records" — that one is unambiguous and is
|
|
23
|
+
* simply imported. It is a PARTIAL store: some records present, and markdown
|
|
24
|
+
* entries that have no record behind them. Any automatic reading of that state
|
|
25
|
+
* is a guess. Importing the strays assumes the markdown is authoritative, which
|
|
26
|
+
* it no longer is. Ignoring them assumes they were deliberately deleted, and
|
|
27
|
+
* projects over them. Both silently discard someone's work in one of the two
|
|
28
|
+
* cases. So it stops and names what it found.
|
|
29
|
+
*
|
|
30
|
+
* That check costs a parse of a small file per mutation, which is the same file
|
|
31
|
+
* the CLI already read on every mutation before the cutover.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
35
|
+
|
|
36
|
+
import { parseIdeabox } from '../ideabox.js';
|
|
37
|
+
import { importIdeabox } from './import-ideabox.js';
|
|
38
|
+
import { KIND } from './provider.js';
|
|
39
|
+
|
|
40
|
+
export class IdeaboxMigrationConflict extends Error {
|
|
41
|
+
constructor(missing, ideaboxPath) {
|
|
42
|
+
super(
|
|
43
|
+
`compose: the ideabox at ${ideaboxPath} contains ${missing.length} idea(s) with no record ` +
|
|
44
|
+
`behind them: ${missing.join(', ')}. The record store is canon now, so this file is ` +
|
|
45
|
+
`generated output — which means these entries were either hand-added after the migration ` +
|
|
46
|
+
`or lost by a partial one, and guessing which would discard someone's work either way. ` +
|
|
47
|
+
`Re-add them with \`compose ideabox add\`, or delete them from the file if they are stale, ` +
|
|
48
|
+
`then run \`compose ideabox render\`.`
|
|
49
|
+
);
|
|
50
|
+
this.name = 'IdeaboxMigrationConflict';
|
|
51
|
+
this.code = 'IDEABOX_MIGRATION_CONFLICT';
|
|
52
|
+
this.missing = missing;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ensure the record store reflects the markdown before any mutation touches it.
|
|
58
|
+
*
|
|
59
|
+
* Runs before every mutating ideabox command. Three states, one of which stops
|
|
60
|
+
* the command:
|
|
61
|
+
*
|
|
62
|
+
* - **no records, markdown has entries** → import it (the upgrade path)
|
|
63
|
+
* - **records exist, markdown adds nothing** → already migrated, proceed
|
|
64
|
+
* - **records exist, markdown has entries with no record** → refuse
|
|
65
|
+
*
|
|
66
|
+
* A fresh project with no markdown and no records is the trivial first case and
|
|
67
|
+
* simply proceeds.
|
|
68
|
+
*
|
|
69
|
+
* @param {import('./provider.js').FluidProvider} provider
|
|
70
|
+
* @param {string} ideaboxPath absolute path to the markdown ideabox
|
|
71
|
+
* @returns {Promise<{migrated: boolean, imported: string[]}>}
|
|
72
|
+
*/
|
|
73
|
+
export async function ensureIdeaboxMigrated(provider, ideaboxPath) {
|
|
74
|
+
const records = await provider.listRecords({ kind: KIND.IDEA });
|
|
75
|
+
const markdown = existsSync(ideaboxPath) ? readFileSync(ideaboxPath, 'utf8') : null;
|
|
76
|
+
|
|
77
|
+
if (markdown === null) return { migrated: false, imported: [] };
|
|
78
|
+
|
|
79
|
+
const parsed = parseIdeabox(markdown);
|
|
80
|
+
const inMarkdown = [...(parsed.ideas ?? []), ...(parsed.killed ?? [])].map((i) => i.id);
|
|
81
|
+
|
|
82
|
+
const known = new Set(records.map((r) => r.handle));
|
|
83
|
+
const missing = inMarkdown.filter((id) => !known.has(id));
|
|
84
|
+
|
|
85
|
+
if (records.length === 0) {
|
|
86
|
+
if (inMarkdown.length === 0) return { migrated: false, imported: [] };
|
|
87
|
+
// The upgrade path.
|
|
88
|
+
const result = await importIdeabox(provider, { markdown, path: ideaboxPath });
|
|
89
|
+
return { migrated: true, imported: result.imported };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (missing.length) {
|
|
93
|
+
// RESUME versus REFUSE, decided PER HANDLE from the events log.
|
|
94
|
+
//
|
|
95
|
+
// A crash partway through the first-use import leaves some records written
|
|
96
|
+
// and the rest missing, which lands here rather than in the empty-store
|
|
97
|
+
// branch above. Refusing that outright strands the installation: the error
|
|
98
|
+
// names `compose ideabox add`, `add` runs this same gate, so every command
|
|
99
|
+
// fails and there is no way out — and the reclaim path built for exactly
|
|
100
|
+
// this case is never reached.
|
|
101
|
+
//
|
|
102
|
+
// The log distinguishes the two populations. A handle the import already
|
|
103
|
+
// burned carries an event; `importIdeabox` skips live records and reclaims
|
|
104
|
+
// its own aborted allocations, so resuming is safe and lossless.
|
|
105
|
+
//
|
|
106
|
+
// The evidence has to be per-handle, not "did an import ever run". Once the
|
|
107
|
+
// first import succeeds the log carries `imported` events forever, so a
|
|
108
|
+
// global check would quietly import anything later hand-added to what is
|
|
109
|
+
// now generated output — losing the very protection this gate exists for.
|
|
110
|
+
// A handle with no event was never issued here: it was typed into the file
|
|
111
|
+
// by hand, and importing it would treat the markdown as authoritative when
|
|
112
|
+
// it no longer is.
|
|
113
|
+
// Three populations, and only one of them is resumable:
|
|
114
|
+
// - issued, no `deleted` event → a create that crashed. RESUME.
|
|
115
|
+
// - issued, `deleted` event → deliberately retired; this file is just
|
|
116
|
+
// stale output. REFUSE (a render fixes it,
|
|
117
|
+
// and importing would resurrect it).
|
|
118
|
+
// - never issued → hand-typed into generated output. REFUSE.
|
|
119
|
+
const { issued, deleted } = await handleHistory(provider);
|
|
120
|
+
const resumable = missing.filter((id) => issued.has(id) && !deleted.has(id));
|
|
121
|
+
const strays = missing.filter((id) => !resumable.includes(id));
|
|
122
|
+
if (strays.length) throw new IdeaboxMigrationConflict(strays, ideaboxPath);
|
|
123
|
+
|
|
124
|
+
const result = await importIdeabox(provider, { markdown, path: ideaboxPath });
|
|
125
|
+
return { migrated: true, imported: result.imported };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { migrated: false, imported: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Which handles this store's log has seen, and which of those were retired.
|
|
133
|
+
*
|
|
134
|
+
* Both sets are empty when the history cannot be read, which makes every
|
|
135
|
+
* missing handle a stray and every ambiguous state a refusal. That is the safe
|
|
136
|
+
* direction: resuming on a guess is the one thing this must not do.
|
|
137
|
+
*/
|
|
138
|
+
async function handleHistory(provider) {
|
|
139
|
+
const empty = { issued: new Set(), deleted: new Set() };
|
|
140
|
+
if (typeof provider.readEvents !== 'function') return empty;
|
|
141
|
+
try {
|
|
142
|
+
const events = (await provider.readEvents()) ?? [];
|
|
143
|
+
const issued = new Set();
|
|
144
|
+
const deleted = new Set();
|
|
145
|
+
for (const event of events) {
|
|
146
|
+
if (!event?.handle) continue;
|
|
147
|
+
issued.add(event.handle);
|
|
148
|
+
if (event.type === 'deleted') deleted.add(event.handle);
|
|
149
|
+
}
|
|
150
|
+
return { issued, deleted };
|
|
151
|
+
} catch {
|
|
152
|
+
return empty;
|
|
153
|
+
}
|
|
154
|
+
}
|