@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
package/server/ideabox-routes.js
CHANGED
|
@@ -1,58 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* server/ideabox-routes.js — REST API for the ideabox feature.
|
|
3
3
|
*
|
|
4
|
+
* COMP-PLAN-IDEA-UNIFY S3b-2: this module reads and writes FLUID RECORDS.
|
|
5
|
+
*
|
|
4
6
|
* Routes:
|
|
5
|
-
* GET /api/ideabox
|
|
6
|
-
* POST /api/ideabox/ideas
|
|
7
|
-
* PATCH /api/ideabox/ideas/:id
|
|
8
|
-
* POST /api/ideabox/ideas/:id/promote
|
|
9
|
-
* POST /api/ideabox/ideas/:id/kill
|
|
10
|
-
*
|
|
7
|
+
* GET /api/ideabox — the whole ideabox, from the records
|
|
8
|
+
* POST /api/ideabox/ideas — capture a new idea
|
|
9
|
+
* PATCH /api/ideabox/ideas/:id — edit fields (priority, effort, impact, …)
|
|
10
|
+
* POST /api/ideabox/ideas/:id/promote — promote to a feature
|
|
11
|
+
* POST /api/ideabox/ideas/:id/kill — kill with a reason
|
|
12
|
+
* POST /api/ideabox/ideas/:id/resurrect — return a killed idea to the live set
|
|
13
|
+
* POST /api/ideabox/ideas/:id/discuss — append to the deliberation trail
|
|
14
|
+
* DELETE /api/ideabox/ideas/:id — not allowed (use kill)
|
|
15
|
+
*
|
|
16
|
+
* WHAT S3b-1 LEFT HERE AND WHY IT IS GONE
|
|
17
|
+
* ---------------------------------------
|
|
18
|
+
* S3b-1 cut the CLI over to the record store and closed all six mutating
|
|
19
|
+
* handlers with a 409, because they rewrote `docs/product/ideabox.md` directly
|
|
20
|
+
* and that file had just become generated output: an idea saved here would have
|
|
21
|
+
* reported success, been overwritten by the next render, and taken the user's
|
|
22
|
+
* text with it. Failing closed was the honest interim state. This slice replaces
|
|
23
|
+
* it with real handlers rather than restoring the old bodies.
|
|
24
|
+
*
|
|
25
|
+
* EVERY MUTATION GOES THROUGH `fluid/ideabox-ops.js`, WHICH THE CLI ALSO USES.
|
|
26
|
+
* Not "the same way the CLI does" — the same code. Two call sites that each had
|
|
27
|
+
* to remember to run the migration gate before writing, and to re-render after,
|
|
28
|
+
* would drift exactly the way `smartmemory-provider.js` drifted from
|
|
29
|
+
* `local-provider.js` in S3b-1: satisfying the interface completely while
|
|
30
|
+
* silently missing a guarantee.
|
|
31
|
+
*
|
|
32
|
+
* Responses come from `fluid/ideabox-view.js`, which projects records into the
|
|
33
|
+
* shape the cockpit and the mobile app have always consumed (`id` is the handle,
|
|
34
|
+
* `description` is the body, an untriaged priority is an em dash).
|
|
11
35
|
*/
|
|
12
36
|
|
|
13
37
|
import fs from 'node:fs'
|
|
14
38
|
import path from 'node:path'
|
|
39
|
+
|
|
15
40
|
import {
|
|
16
|
-
|
|
17
|
-
writeIdeabox,
|
|
41
|
+
ideaboxContext,
|
|
18
42
|
addIdea,
|
|
19
|
-
|
|
43
|
+
addDiscussion,
|
|
20
44
|
killIdea,
|
|
45
|
+
promoteIdea,
|
|
21
46
|
resurrectIdea,
|
|
22
|
-
setPriority,
|
|
23
47
|
updateIdea,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
IdeaboxConflict,
|
|
49
|
+
IdeaboxInvalid,
|
|
50
|
+
IdeaboxNotFound,
|
|
51
|
+
IdeaboxRenderFailed,
|
|
52
|
+
} from '../lib/fluid/ideabox-ops.js'
|
|
53
|
+
import { writeIdeaboxProjection } from '../lib/fluid/render-ideabox.js'
|
|
54
|
+
import { ideaboxView, toClientIdeaWith } from '../lib/fluid/ideabox-view.js'
|
|
55
|
+
import { relForDisplay } from '../lib/project-paths.js'
|
|
29
56
|
|
|
30
57
|
/**
|
|
31
58
|
* @param {object} app — Express app
|
|
32
59
|
* @param {{ getProjectRoot, getDataDir, broadcastMessage }} deps
|
|
33
60
|
*/
|
|
34
|
-
export function attachIdeaboxRoutes(app, { getProjectRoot,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
export function attachIdeaboxRoutes(app, { getProjectRoot, broadcastMessage }) {
|
|
62
|
+
/**
|
|
63
|
+
* A fresh context per request, deliberately.
|
|
64
|
+
*
|
|
65
|
+
* The provider holds paths, not state — every handle lookup re-reads the
|
|
66
|
+
* records directory and the events log — so caching one would buy nothing and
|
|
67
|
+
* cost correctness the moment the project root changes underneath a long-lived
|
|
68
|
+
* server, which is the case this file has always had to handle.
|
|
69
|
+
*/
|
|
70
|
+
function context() {
|
|
41
71
|
const projectRoot = getProjectRoot()
|
|
42
|
-
|
|
43
|
-
if (!_cache || _lastProjectRoot !== projectRoot || _lastDataDir !== dataDir) {
|
|
44
|
-
const config = loadConfig(projectRoot)
|
|
45
|
-
const sourceFile = resolveIdeaboxPathFromConfig(projectRoot, config)
|
|
46
|
-
_cache = new IdeaboxCache(dataDir, sourceFile)
|
|
47
|
-
_lastProjectRoot = projectRoot
|
|
48
|
-
_lastDataDir = dataDir
|
|
49
|
-
}
|
|
50
|
-
return _cache
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getIdeaboxPath(projectRoot) {
|
|
54
|
-
const config = loadConfig(projectRoot)
|
|
55
|
-
return config?.paths?.ideabox || 'docs/product/ideabox.md'
|
|
72
|
+
return ideaboxContext(projectRoot, { config: loadConfig(projectRoot) ?? {}, origin: 'ui:ideabox' })
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
function broadcastUpdate() {
|
|
@@ -61,220 +78,145 @@ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcast
|
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Map an op failure onto HTTP.
|
|
83
|
+
*
|
|
84
|
+
* `IdeaboxRenderFailed` is deliberately NOT an error response. The record is
|
|
85
|
+
* durable; only the generated markdown is stale. Both clients roll their
|
|
86
|
+
* optimistic update back on any non-ok response, so reporting a failure here
|
|
87
|
+
* would make a committed idea vanish from the UI and invite the user to type
|
|
88
|
+
* it again — producing the duplicate the whole record store exists to prevent.
|
|
89
|
+
* It is answered as a success carrying `projectionStale`, so a client that
|
|
90
|
+
* wants to surface the staleness can, and one that ignores unknown fields
|
|
91
|
+
* behaves correctly by default.
|
|
92
|
+
*/
|
|
93
|
+
async function send(res, run, okStatus = 200) {
|
|
66
94
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
res.json(
|
|
95
|
+
const { body, broadcast = true } = await run()
|
|
96
|
+
if (broadcast) broadcastUpdate()
|
|
97
|
+
res.status(okStatus).json(body)
|
|
70
98
|
} catch (err) {
|
|
71
|
-
|
|
99
|
+
if (err instanceof IdeaboxRenderFailed) {
|
|
100
|
+
// The write landed. Tell the truth about both halves.
|
|
101
|
+
broadcastUpdate()
|
|
102
|
+
const idea = err.record ? await safeIdea(err.record) : null
|
|
103
|
+
return res.status(okStatus).json({
|
|
104
|
+
...(idea ?? {}),
|
|
105
|
+
projectionStale: true,
|
|
106
|
+
warning: err.message,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
if (err instanceof IdeaboxNotFound) return res.status(404).json({ error: err.message, code: err.code })
|
|
110
|
+
if (err instanceof IdeaboxInvalid) return res.status(400).json({ error: err.message, code: err.code, field: err.field })
|
|
111
|
+
if (err instanceof IdeaboxConflict) return res.status(409).json({ error: err.message, code: err.code })
|
|
112
|
+
return res.status(500).json({ error: err.message })
|
|
72
113
|
}
|
|
73
|
-
}
|
|
114
|
+
}
|
|
74
115
|
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
/** Best-effort client projection for the render-failed path, which must not
|
|
117
|
+
* fail a second time on the way out. */
|
|
118
|
+
async function safeIdea(record) {
|
|
77
119
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
120
|
+
const ctx = await context()
|
|
121
|
+
return await toClientIdeaWith(ctx.provider, record)
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
80
126
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
res.status(201).json(newIdea)
|
|
127
|
+
// GET /api/ideabox — served from the records, per the feature's acceptance
|
|
128
|
+
// criterion. NOT from `docs/product/ideabox.md`: that file is a local
|
|
129
|
+
// projection, and on a store shared across machines (the SmartMemory
|
|
130
|
+
// provider) a write on one machine never regenerates another's copy, so
|
|
131
|
+
// reading it would serve an indefinitely stale view of a current store.
|
|
132
|
+
app.get('/api/ideabox', async (_req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const ctx = await context()
|
|
135
|
+
res.json(await ideaboxView(ctx.provider))
|
|
91
136
|
} catch (err) {
|
|
92
137
|
res.status(500).json({ error: err.message })
|
|
93
138
|
}
|
|
94
139
|
})
|
|
95
140
|
|
|
96
|
-
//
|
|
97
|
-
app.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Handle priority shortcut
|
|
107
|
-
if (fields.priority) {
|
|
108
|
-
setPriority(parsed, id, fields.priority)
|
|
109
|
-
delete fields.priority
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Reject status changes via PATCH — must use /promote or /kill endpoints
|
|
113
|
-
// to ensure proper transition logic (move to killed section, set fields, etc.)
|
|
114
|
-
if (fields.status !== undefined) {
|
|
115
|
-
return res.status(400).json({
|
|
116
|
-
error: 'Status changes must go through /promote or /kill endpoints, not PATCH'
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Handle remaining fields (no status)
|
|
121
|
-
const allowed = ['title', 'description', 'source', 'tags', 'cluster', 'mapsTo', 'effort', 'impact']
|
|
122
|
-
const safeFields = {}
|
|
123
|
-
for (const k of allowed) {
|
|
124
|
-
if (fields[k] !== undefined) safeFields[k] = fields[k]
|
|
125
|
-
}
|
|
126
|
-
// Validate enum fields
|
|
127
|
-
if (safeFields.effort !== undefined && safeFields.effort !== null && !['S', 'M', 'L'].includes(safeFields.effort)) {
|
|
128
|
-
return res.status(400).json({ error: 'effort must be S, M, L, or null' })
|
|
129
|
-
}
|
|
130
|
-
if (safeFields.impact !== undefined && safeFields.impact !== null && !['low', 'medium', 'high'].includes(safeFields.impact)) {
|
|
131
|
-
return res.status(400).json({ error: 'impact must be low, medium, high, or null' })
|
|
132
|
-
}
|
|
133
|
-
if (Object.keys(safeFields).length > 0) {
|
|
134
|
-
updateIdea(parsed, id, safeFields)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
writeIdeabox(projectRoot, ideaboxPath, parsed)
|
|
138
|
-
getCache().invalidate()
|
|
139
|
-
broadcastUpdate()
|
|
141
|
+
// POST /api/ideabox/ideas
|
|
142
|
+
app.post('/api/ideabox/ideas', async (req, res) => {
|
|
143
|
+
const { title, description, source, tags, cluster } = req.body || {}
|
|
144
|
+
await send(res, async () => {
|
|
145
|
+
const ctx = await context()
|
|
146
|
+
const { record } = await addIdea(ctx, { title, body: description, source, tags, cluster })
|
|
147
|
+
return { body: await toClientIdeaWith(ctx.provider, record) }
|
|
148
|
+
}, 201)
|
|
149
|
+
})
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
151
|
+
// PATCH /api/ideabox/ideas/:id
|
|
152
|
+
app.patch('/api/ideabox/ideas/:id', async (req, res) => {
|
|
153
|
+
await send(res, async () => {
|
|
154
|
+
const ctx = await context()
|
|
155
|
+
const { record } = await updateIdea(ctx, req.params.id, req.body || {})
|
|
156
|
+
return { body: await toClientIdeaWith(ctx.provider, record) }
|
|
157
|
+
})
|
|
149
158
|
})
|
|
150
159
|
|
|
151
160
|
// POST /api/ideabox/ideas/:id/promote
|
|
152
|
-
app.post('/api/ideabox/ideas/:id/promote', (req, res) => {
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const { featureCode } =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Resolve feature code: explicit, or derived from idea
|
|
169
|
-
let resolvedCode = featureCode
|
|
170
|
-
if (!resolvedCode) {
|
|
171
|
-
const slug = sourceIdea.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20).replace(/-+$/, '')
|
|
172
|
-
resolvedCode = `IDEA-${sourceIdea.num}-${slug}`.toUpperCase()
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Create feature folder + feature.json (same logic as CLI promote).
|
|
176
|
-
// COMP-PATHS-EXTERNAL: resolve the ABSOLUTE features base (may be
|
|
177
|
-
// relocated outside projectRoot). Re-rooting under projectRoot would make
|
|
178
|
-
// existsSync check the wrong path and risk overwriting external canon.
|
|
179
|
-
const composeJsonPath = path.join(projectRoot, '.compose', 'compose.json')
|
|
180
|
-
let cfg = {}
|
|
181
|
-
if (fs.existsSync(composeJsonPath)) {
|
|
182
|
-
try { cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8')) } catch {}
|
|
183
|
-
}
|
|
184
|
-
const featuresBase = resolveFeaturesPathFromConfig(projectRoot, cfg) // absolute
|
|
185
|
-
const featuresDir = path.join(featuresBase, resolvedCode)
|
|
186
|
-
if (!fs.existsSync(featuresDir)) {
|
|
187
|
-
// COMP-MCP-VALIDATE-1: route through the validated writer instead of a
|
|
188
|
-
// raw fs.writeFileSync so the promoted feature.json is schema-guarded.
|
|
189
|
-
writeFeature(projectRoot, {
|
|
190
|
-
code: resolvedCode,
|
|
191
|
-
description: sourceIdea.title,
|
|
192
|
-
status: 'PLANNED',
|
|
193
|
-
promotedFrom: sourceIdea.id,
|
|
194
|
-
createdAt: new Date().toISOString(),
|
|
195
|
-
}, featuresBase)
|
|
161
|
+
app.post('/api/ideabox/ideas/:id/promote', async (req, res) => {
|
|
162
|
+
await send(res, async () => {
|
|
163
|
+
const ctx = await context()
|
|
164
|
+
const { record, featureCode, featurePath } = await promoteIdea(
|
|
165
|
+
ctx, req.params.id, (req.body || {}).featureCode || ''
|
|
166
|
+
)
|
|
167
|
+
// `featureCode` and `featurePath` are a promotion-specific envelope around
|
|
168
|
+
// the idea, not fields of it. The mobile client reads `result.featureCode`
|
|
169
|
+
// to name what it just created, and it matters most in exactly the case
|
|
170
|
+
// where the client did not supply one and the server derived it.
|
|
171
|
+
return {
|
|
172
|
+
body: {
|
|
173
|
+
...(await toClientIdeaWith(ctx.provider, record)),
|
|
174
|
+
featureCode,
|
|
175
|
+
featurePath: relForDisplay(ctx.cwd, featurePath),
|
|
176
|
+
},
|
|
196
177
|
}
|
|
197
|
-
|
|
198
|
-
promoteIdea(parsed, id, resolvedCode)
|
|
199
|
-
writeIdeabox(projectRoot, ideaboxPath, parsed)
|
|
200
|
-
getCache().invalidate()
|
|
201
|
-
broadcastUpdate()
|
|
202
|
-
|
|
203
|
-
const updated = parsed.ideas.find(i => i.id.toUpperCase() === upper)
|
|
204
|
-
res.json({ ...(updated || { id }), featureCode: resolvedCode, featurePath: relForDisplay(projectRoot, featuresDir) })
|
|
205
|
-
} catch (err) {
|
|
206
|
-
const status = err.message.includes('not found') ? 404 : 500
|
|
207
|
-
res.status(status).json({ error: err.message })
|
|
208
|
-
}
|
|
178
|
+
})
|
|
209
179
|
})
|
|
210
180
|
|
|
211
181
|
// POST /api/ideabox/ideas/:id/kill
|
|
212
|
-
app.post('/api/ideabox/ideas/:id/kill', (req, res) => {
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const ideaboxPath = getIdeaboxPath(projectRoot)
|
|
219
|
-
const parsed = readIdeabox(projectRoot, ideaboxPath)
|
|
220
|
-
killIdea(parsed, id, reason || '')
|
|
221
|
-
writeIdeabox(projectRoot, ideaboxPath, parsed)
|
|
222
|
-
getCache().invalidate()
|
|
223
|
-
broadcastUpdate()
|
|
224
|
-
|
|
225
|
-
const upper = id.toUpperCase()
|
|
226
|
-
const killed = parsed.killed.find(i => i.id.toUpperCase() === upper)
|
|
227
|
-
res.json(killed || { id })
|
|
228
|
-
} catch (err) {
|
|
229
|
-
const status = err.message.includes('not found') ? 404 : 500
|
|
230
|
-
res.status(status).json({ error: err.message })
|
|
231
|
-
}
|
|
182
|
+
app.post('/api/ideabox/ideas/:id/kill', async (req, res) => {
|
|
183
|
+
await send(res, async () => {
|
|
184
|
+
const ctx = await context()
|
|
185
|
+
const { record } = await killIdea(ctx, req.params.id, (req.body || {}).reason || '')
|
|
186
|
+
return { body: await toClientIdeaWith(ctx.provider, record) }
|
|
187
|
+
})
|
|
232
188
|
})
|
|
233
189
|
|
|
234
190
|
// POST /api/ideabox/ideas/:id/resurrect
|
|
235
|
-
app.post('/api/ideabox/ideas/:id/resurrect', (req, res) => {
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
resurrectIdea(parsed, id)
|
|
242
|
-
writeIdeabox(projectRoot, ideaboxPath, parsed)
|
|
243
|
-
getCache().invalidate()
|
|
244
|
-
broadcastUpdate()
|
|
245
|
-
|
|
246
|
-
const upper = id.toUpperCase()
|
|
247
|
-
const restored = parsed.ideas.find(i => i.id.toUpperCase() === upper)
|
|
248
|
-
res.json(restored || { id })
|
|
249
|
-
} catch (err) {
|
|
250
|
-
const status = err.message.includes('not found') ? 404 : 500
|
|
251
|
-
res.status(status).json({ error: err.message })
|
|
252
|
-
}
|
|
191
|
+
app.post('/api/ideabox/ideas/:id/resurrect', async (req, res) => {
|
|
192
|
+
await send(res, async () => {
|
|
193
|
+
const ctx = await context()
|
|
194
|
+
const { record } = await resurrectIdea(ctx, req.params.id)
|
|
195
|
+
return { body: await toClientIdeaWith(ctx.provider, record) }
|
|
196
|
+
})
|
|
253
197
|
})
|
|
254
198
|
|
|
255
199
|
// POST /api/ideabox/ideas/:id/discuss
|
|
256
|
-
app.post('/api/ideabox/ideas/:id/discuss', (req, res) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const parsed = readIdeabox(projectRoot, ideaboxPath)
|
|
266
|
-
addDiscussion(parsed, id, author, text)
|
|
267
|
-
writeIdeabox(projectRoot, ideaboxPath, parsed)
|
|
268
|
-
getCache().invalidate()
|
|
269
|
-
broadcastUpdate()
|
|
200
|
+
app.post('/api/ideabox/ideas/:id/discuss', async (req, res) => {
|
|
201
|
+
const { author, text } = req.body || {}
|
|
202
|
+
if (!author) return res.status(400).json({ error: 'author is required' })
|
|
203
|
+
await send(res, async () => {
|
|
204
|
+
const ctx = await context()
|
|
205
|
+
const { record } = await addDiscussion(ctx, req.params.id, { author, text })
|
|
206
|
+
return { body: await toClientIdeaWith(ctx.provider, record) }
|
|
207
|
+
}, 201)
|
|
208
|
+
})
|
|
270
209
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
210
|
+
// POST /api/ideabox/render — rebuild the projection from the records (the
|
|
211
|
+
// repair for a landed-unrendered write: the durable record is fine, only the
|
|
212
|
+
// generated file is stale). The HTTP twin of `compose ideabox render`;
|
|
213
|
+
// touches no record. (FOH-6 S4 — the colleague panel's repair affordance.)
|
|
214
|
+
app.post('/api/ideabox/render', async (_req, res) => {
|
|
215
|
+
await send(res, async () => {
|
|
216
|
+
const ctx = await context()
|
|
217
|
+
await writeIdeaboxProjection(ctx.provider, ctx.ideaboxPath)
|
|
218
|
+
return { body: { ok: true } }
|
|
219
|
+
})
|
|
278
220
|
})
|
|
279
221
|
|
|
280
222
|
// DELETE /api/ideabox/ideas/:id — not allowed
|