@smartmemory/compose 0.3.6-beta → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.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-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- 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 +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- 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/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- 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 +66 -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 -14
- 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/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- 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 +428 -153
- package/lib/review-normalize.js +4 -0
- 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 +391 -31
- 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 +124 -24
- package/server/compose-mcp.js +91 -790
- 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 +35 -3
- 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-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/ideabox-view.js — fluid records → the ideabox's client-facing shape.
|
|
3
|
+
*
|
|
4
|
+
* COMP-PLAN-IDEA-UNIFY S3b-2 (D21, reversed after review).
|
|
5
|
+
*
|
|
6
|
+
* THE ONE MAPPING
|
|
7
|
+
* ---------------
|
|
8
|
+
* `/api/ideabox` and every mutation response are produced HERE, from records.
|
|
9
|
+
* The first draft of this slice derived them by parsing the markdown projection
|
|
10
|
+
* the write had just rendered — one shape, reusing a proven parser, no second
|
|
11
|
+
* adapter to drift. That was wrong for two reasons that outrank tidiness:
|
|
12
|
+
*
|
|
13
|
+
* 1. **The feature's acceptance criterion says otherwise** ("`useIdeaboxStore`
|
|
14
|
+
* / `/api/ideabox` serve from fluid records", design.md). Parsing the
|
|
15
|
+
* projection is serving from a rendering of the records, which is not the
|
|
16
|
+
* same claim.
|
|
17
|
+
* 2. **It is only correct on the local provider.** The projection is a LOCAL
|
|
18
|
+
* file. Configure the SmartMemory provider — a store shared across machines
|
|
19
|
+
* — and a write on machine A never regenerates machine B's markdown, so B's
|
|
20
|
+
* REST and UI serve an indefinitely stale view of a store that is perfectly
|
|
21
|
+
* up to date. Fidelity of the projection says nothing about its freshness.
|
|
22
|
+
*
|
|
23
|
+
* The cost of this direction is the one the first draft was avoiding: this file
|
|
24
|
+
* is a second place where a record's fields become a client's fields, and a
|
|
25
|
+
* field added to the record contract but not here is invisible to the cockpit.
|
|
26
|
+
* That is bounded by a test asserting this projection and the markdown one carry
|
|
27
|
+
* the same field set, so the two cannot silently disagree.
|
|
28
|
+
*
|
|
29
|
+
* WHY THE SHAPE IS THE LEGACY PARSER'S AND NOT THE RECORD'S
|
|
30
|
+
* --------------------------------------------------------
|
|
31
|
+
* `id` here is the record's HANDLE (`IDEA-42`), not its `id` (a provider UUID
|
|
32
|
+
* that changes on a provider swap). Every client keys on the handle and always
|
|
33
|
+
* has. `description` is the record's `body`, `status` is the uppercase display
|
|
34
|
+
* token, and an untriaged priority is an em dash rather than null. Returning raw
|
|
35
|
+
* records would have been cleaner and would have broken the cockpit, the mobile
|
|
36
|
+
* app and their tests in the same commit.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { toMarkdownDate } from './ideabox-dates.js';
|
|
40
|
+
import { KIND } from './provider.js';
|
|
41
|
+
|
|
42
|
+
/** Canonical status → the display token the surfaces have always rendered. */
|
|
43
|
+
const STATUS_TOKEN = Object.freeze({
|
|
44
|
+
new: 'NEW',
|
|
45
|
+
discussing: 'DISCUSSING',
|
|
46
|
+
promoted: 'PROMOTED',
|
|
47
|
+
killed: 'KILLED',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The numeric suffix of a handle, which clients sort and display as `num`. */
|
|
51
|
+
export function handleNumber(handle) {
|
|
52
|
+
const m = /-([0-9]+)$/.exec(handle ?? '');
|
|
53
|
+
return m ? Number(m[1]) : 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One record → one client idea.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} record a normalized fluid record
|
|
60
|
+
* @param {Map<string,string>} clusterTitles handle → display title. Clients show
|
|
61
|
+
* and filter on the cluster's NAME, which is what the markdown surface always
|
|
62
|
+
* gave them; the record stores a handle so that renaming an umbrella does not
|
|
63
|
+
* orphan its members. Resolution happens here rather than at the call site so
|
|
64
|
+
* no caller can forget it and leak `CLUS-3` into the UI.
|
|
65
|
+
*/
|
|
66
|
+
export function toClientIdea(record, clusterTitles = new Map()) {
|
|
67
|
+
return {
|
|
68
|
+
id: record.handle,
|
|
69
|
+
num: handleNumber(record.handle),
|
|
70
|
+
title: record.title,
|
|
71
|
+
// `status_label` preserves what an author wrote when the canonical enum
|
|
72
|
+
// could not hold it (`RE-AIMED (2026-07-21)`); the markdown surface shows
|
|
73
|
+
// that token, so this one must too.
|
|
74
|
+
status: record.status_label || STATUS_TOKEN[record.status] || 'NEW',
|
|
75
|
+
priority: record.priority ?? '—',
|
|
76
|
+
tags: [...(record.tags ?? [])],
|
|
77
|
+
source: record.source ?? '',
|
|
78
|
+
description: record.body ?? '',
|
|
79
|
+
cluster: record.cluster ? clusterTitles.get(record.cluster) ?? record.cluster : null,
|
|
80
|
+
clusterHandle: record.cluster ?? null,
|
|
81
|
+
mapsTo: record.links?.find((l) => l.type === 'maps_to')?.target ?? '',
|
|
82
|
+
promotedTo: record.links?.find((l) => l.type === 'promoted_to')?.target ?? '',
|
|
83
|
+
effort: record.effort ?? null,
|
|
84
|
+
impact: record.impact ?? null,
|
|
85
|
+
killedReason: record.killed?.reason ?? '',
|
|
86
|
+
killedDate: record.killed ? toMarkdownDate(record.killed.at) : '',
|
|
87
|
+
discussion: (record.discussion ?? []).map((d) => ({
|
|
88
|
+
date: toMarkdownDate(d.at),
|
|
89
|
+
author: d.author ?? 'unknown',
|
|
90
|
+
text: d.text,
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Cluster handle → title, for `toClientIdea`. */
|
|
96
|
+
export function clusterTitleMap(clusters) {
|
|
97
|
+
return new Map((clusters ?? []).map((c) => [c.handle, c.title]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The whole `/api/ideabox` payload, read from the provider.
|
|
102
|
+
*
|
|
103
|
+
* Live and killed ideas are separate arrays because that is the split every
|
|
104
|
+
* client already renders, and `nextId` is derived rather than stored — the
|
|
105
|
+
* markdown parser computed it the same way, and a stored counter would be a
|
|
106
|
+
* second allocator disagreeing with the provider's.
|
|
107
|
+
*
|
|
108
|
+
* @param {import('./provider.js').FluidProvider} provider
|
|
109
|
+
*/
|
|
110
|
+
export async function ideaboxView(provider) {
|
|
111
|
+
const [ideas, clusters] = await Promise.all([
|
|
112
|
+
provider.listRecords({ kind: KIND.IDEA }),
|
|
113
|
+
provider.listRecords({ kind: KIND.CLUSTER }),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const titles = clusterTitleMap(clusters);
|
|
117
|
+
const byHandle = (a, b) => handleNumber(a.handle) - handleNumber(b.handle);
|
|
118
|
+
const ordered = [...ideas].sort(byHandle);
|
|
119
|
+
|
|
120
|
+
const maxNum = ordered.reduce((m, r) => Math.max(m, handleNumber(r.handle)), 0);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
ideas: ordered.filter((r) => r.status !== 'killed').map((r) => toClientIdea(r, titles)),
|
|
124
|
+
killed: ordered.filter((r) => r.status === 'killed').map((r) => toClientIdea(r, titles)),
|
|
125
|
+
nextId: maxNum + 1,
|
|
126
|
+
clusters: [...clusters]
|
|
127
|
+
.sort((a, b) => (a.cluster_order ?? Number.MAX_SAFE_INTEGER) - (b.cluster_order ?? Number.MAX_SAFE_INTEGER)
|
|
128
|
+
|| handleNumber(a.handle) - handleNumber(b.handle))
|
|
129
|
+
.map((c) => ({ handle: c.handle, name: c.title, theme: c.body ?? '', order: c.cluster_order ?? null })),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One record → the client shape, resolving its cluster title from the provider.
|
|
135
|
+
*
|
|
136
|
+
* The convenience form for a mutation response, which has one record and no
|
|
137
|
+
* cluster list in hand. `listRecords` for clusters is a handful of small reads
|
|
138
|
+
* on the floor and one call on a remote provider, and getting the name right
|
|
139
|
+
* matters more: a response whose `cluster` is `CLUS-3` where the hydrate says
|
|
140
|
+
* `Umbrella A` makes an optimistic client redraw the idea into a group that does
|
|
141
|
+
* not exist.
|
|
142
|
+
*/
|
|
143
|
+
export async function toClientIdeaWith(provider, record) {
|
|
144
|
+
if (!record?.cluster) return toClientIdea(record);
|
|
145
|
+
return toClientIdea(record, clusterTitleMap(await provider.listRecords({ kind: KIND.CLUSTER })));
|
|
146
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/fluid/import-ideabox.js — one-time markdown → fluid records import.
|
|
3
|
+
*
|
|
4
|
+
* COMP-PLAN-IDEA-UNIFY S2. `ONE-WAY-WRITES`: this is an import, executed once,
|
|
5
|
+
* NOT a sync. There is no reverse path and there must never be one — the
|
|
6
|
+
* ideabox↔vision-store fragmentation this epic exists to end was created by
|
|
7
|
+
* exactly that kind of two-way bridge. After the import, `ideabox.md` is a
|
|
8
|
+
* projection (see render-ideabox.js) and the records are canon.
|
|
9
|
+
*
|
|
10
|
+
* The import is deliberately conservative about identity:
|
|
11
|
+
* - existing `IDEA-N` handles are carried over VERBATIM, because they are
|
|
12
|
+
* cited in docs, commits and conversation (the substrate ruling itself
|
|
13
|
+
* cites IDEA-20). A migration that renumbered would silently invalidate
|
|
14
|
+
* every one of those references.
|
|
15
|
+
* - every imported record is stamped `origin: import:ideabox`, so a migrated
|
|
16
|
+
* row stays distinguishable from a natively captured one for the rest of
|
|
17
|
+
* its life. Provenance is captured at write time and never retrofitted.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
import { parseIdeabox } from '../ideabox.js';
|
|
23
|
+
import { toRecordTimestamp } from './ideabox-dates.js';
|
|
24
|
+
import { KIND } from './provider.js';
|
|
25
|
+
|
|
26
|
+
/** Markdown status token → canonical fluid status. */
|
|
27
|
+
const STATUS_MAP = Object.freeze({
|
|
28
|
+
NEW: 'new',
|
|
29
|
+
DISCUSSING: 'discussing',
|
|
30
|
+
PROMOTED: 'promoted',
|
|
31
|
+
KILLED: 'killed',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The ideabox writes free-form status values (`RE-AIMED (2026-07-21)`,
|
|
36
|
+
* `PROMOTED (→ FEAT-1)`). Map on the leading token and keep the full original
|
|
37
|
+
* so nothing is silently normalized away.
|
|
38
|
+
*/
|
|
39
|
+
function toStatus(raw) {
|
|
40
|
+
const token = String(raw ?? '').trim().split(/[\s(]/)[0].toUpperCase();
|
|
41
|
+
return STATUS_MAP[token] ?? 'new';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toPriority(raw) {
|
|
45
|
+
const p = String(raw ?? '').trim();
|
|
46
|
+
return /^P[012]$/.test(p) ? p : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Import an ideabox markdown document into a fluid provider.
|
|
51
|
+
*
|
|
52
|
+
* @param {import('./provider.js').FluidProvider} provider
|
|
53
|
+
* @param {object} opts
|
|
54
|
+
* @param {string} [opts.markdown] document contents
|
|
55
|
+
* @param {string} [opts.path] read the document from here instead
|
|
56
|
+
* @param {boolean} [opts.dryRun] compute the plan without writing
|
|
57
|
+
* @returns {Promise<{imported: string[], skipped: string[], clusters: string[], alreadyImported: boolean}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function importIdeabox(provider, { markdown, path, dryRun = false } = {}) {
|
|
60
|
+
const source = markdown ?? readFileSync(path, 'utf8');
|
|
61
|
+
const parsed = parseIdeabox(source);
|
|
62
|
+
|
|
63
|
+
const provenance = { origin: 'import:ideabox' };
|
|
64
|
+
|
|
65
|
+
// Idempotence: a handle already present is left ALONE, not overwritten.
|
|
66
|
+
// Re-running must not clobber edits made through the tools after the first
|
|
67
|
+
// import, and must not throw on the handle-already-issued guard.
|
|
68
|
+
const existing = new Set((await provider.listRecords()).map((r) => r.handle));
|
|
69
|
+
|
|
70
|
+
const imported = [];
|
|
71
|
+
const skipped = [];
|
|
72
|
+
const clusterHandles = [];
|
|
73
|
+
|
|
74
|
+
// ---- clusters first: members reference them by handle --------------------
|
|
75
|
+
const clusterHandleByName = new Map();
|
|
76
|
+
for (const cluster of parsed.clusters ?? []) {
|
|
77
|
+
const known = (await provider.listRecords({ kind: KIND.CLUSTER }))
|
|
78
|
+
.find((r) => r.title === cluster.name);
|
|
79
|
+
if (known) {
|
|
80
|
+
clusterHandleByName.set(cluster.name, known.handle);
|
|
81
|
+
skipped.push(known.handle);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
clusterHandleByName.set(cluster.name, `(new cluster) ${cluster.name}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const rec = await provider.createRecord({
|
|
89
|
+
kind: KIND.CLUSTER,
|
|
90
|
+
title: cluster.name,
|
|
91
|
+
// The umbrella's hand-authored Theme paragraph. This is the field the
|
|
92
|
+
// whole cluster-as-record decision exists for.
|
|
93
|
+
body: cluster.theme ?? '',
|
|
94
|
+
cluster_order: cluster.order,
|
|
95
|
+
provenance,
|
|
96
|
+
// See below — the import is the one caller that must survive a rerun.
|
|
97
|
+
reclaimAborted: true,
|
|
98
|
+
});
|
|
99
|
+
clusterHandleByName.set(cluster.name, rec.handle);
|
|
100
|
+
clusterHandles.push(rec.handle);
|
|
101
|
+
imported.push(rec.handle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- ideas ---------------------------------------------------------------
|
|
105
|
+
const all = [
|
|
106
|
+
...(parsed.ideas ?? []).map((i) => ({ idea: i, killed: false })),
|
|
107
|
+
...(parsed.killed ?? []).map((i) => ({ idea: i, killed: true })),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
for (const { idea, killed } of all) {
|
|
111
|
+
if (existing.has(idea.id)) {
|
|
112
|
+
skipped.push(idea.id);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const clusterHandle = idea.cluster ? clusterHandleByName.get(idea.cluster) ?? null : null;
|
|
116
|
+
const clusterOrder = idea.cluster
|
|
117
|
+
? (parsed.clusters ?? []).find((c) => c.name === idea.cluster)?.order ?? null
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
const record = {
|
|
121
|
+
kind: KIND.IDEA,
|
|
122
|
+
// Verbatim. The whole point of the caller-supplied handle path.
|
|
123
|
+
handle: idea.id,
|
|
124
|
+
title: idea.title,
|
|
125
|
+
body: idea.description ?? '',
|
|
126
|
+
status: killed ? 'killed' : toStatus(idea.status),
|
|
127
|
+
// Keep the author's token when the canonical enum cannot hold it, so a
|
|
128
|
+
// closed enum does not quietly flatten `RE-AIMED (2026-07-21)` to `NEW`.
|
|
129
|
+
status_label: STATUS_MAP[String(idea.status ?? '').trim().toUpperCase()]
|
|
130
|
+
? null
|
|
131
|
+
: (idea.status || null),
|
|
132
|
+
priority: toPriority(idea.priority),
|
|
133
|
+
// Carried, not dropped. `parseIdeabox` already validates both against
|
|
134
|
+
// their enums and yields null otherwise (`lib/ideabox.js:304-311`), so
|
|
135
|
+
// there is nothing to re-check here — but omitting them is not a harmless
|
|
136
|
+
// gap. This function is the first-use migration gate every upgrading
|
|
137
|
+
// install runs, and the render that follows it rewrites the markdown from
|
|
138
|
+
// the records. A dropped field is therefore deleted from the user's file
|
|
139
|
+
// on upgrade, silently, with no way back. No idea in THIS repo carries
|
|
140
|
+
// either, which is exactly why it went unnoticed.
|
|
141
|
+
effort: idea.effort ?? null,
|
|
142
|
+
impact: idea.impact ?? null,
|
|
143
|
+
cluster: clusterHandle,
|
|
144
|
+
cluster_order: clusterOrder,
|
|
145
|
+
tags: idea.tags ?? [],
|
|
146
|
+
source: idea.source || null,
|
|
147
|
+
links: idea.mapsTo ? [{ type: 'maps_to', target: idea.mapsTo }] : [],
|
|
148
|
+
killed: (killed || idea.killedReason)
|
|
149
|
+
? {
|
|
150
|
+
at: toRecordTimestamp(idea.killedDate),
|
|
151
|
+
reason: idea.killedReason || 'reason not recorded in markdown',
|
|
152
|
+
}
|
|
153
|
+
: null,
|
|
154
|
+
discussion: (idea.discussion ?? []).map((d) => ({
|
|
155
|
+
at: toRecordTimestamp(d.date),
|
|
156
|
+
text: d.text ?? '',
|
|
157
|
+
author: d.author ?? null,
|
|
158
|
+
})),
|
|
159
|
+
provenance,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
imported.push(idea.id);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// `reclaimAborted` makes the import RESTARTABLE. Creation burns the handle
|
|
167
|
+
// before writing the record, and this loop skips only handles with a live
|
|
168
|
+
// record — so a crash between those two steps leaves a handle that is
|
|
169
|
+
// issued, absent, and permanently un-creatable. Without this flag the
|
|
170
|
+
// one-time migration of a project's entire idea corpus cannot be rerun
|
|
171
|
+
// after a partial failure, which is the failure it is most likely to have.
|
|
172
|
+
// Narrow by construction: the provider reclaims ONLY a handle that was
|
|
173
|
+
// never live and never deleted, and no other caller passes this.
|
|
174
|
+
await provider.createRecord({ ...record, reclaimAborted: true });
|
|
175
|
+
imported.push(idea.id);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
imported,
|
|
180
|
+
skipped,
|
|
181
|
+
clusters: clusterHandles,
|
|
182
|
+
// True when the document had content and every handle in it was already
|
|
183
|
+
// present — i.e. this was a re-run, not a first import.
|
|
184
|
+
alreadyImported: imported.length === 0 && skipped.length > 0,
|
|
185
|
+
};
|
|
186
|
+
}
|