@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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
iterationDecisionEventId,
|
|
22
22
|
gateDecisionEventId,
|
|
23
23
|
driftThresholdDecisionEventId,
|
|
24
|
+
policyViolationDecisionEventId,
|
|
24
25
|
} from './decision-event-id.js';
|
|
25
26
|
import { mapResolveOutcomeToSchema } from './gate-log-store.js';
|
|
26
27
|
|
|
@@ -43,13 +44,15 @@ export function emitDecisionEvent(broadcastMessage, event) {
|
|
|
43
44
|
/**
|
|
44
45
|
* Build a kind=phase_transition DecisionEvent.
|
|
45
46
|
*
|
|
46
|
-
* @param {{ featureCode, from, to, outcome, agent_id, timestamp }} params
|
|
47
|
+
* @param {{ featureCode, from, to, outcome, agent_id, timestamp, origin, recordedAt, confidence }} params
|
|
47
48
|
* - from: previous phase string, or null for the initial lifecycle start
|
|
48
49
|
* - to: new phase string
|
|
49
50
|
* - outcome: optional outcome string (for context)
|
|
50
51
|
* - agent_id: optional operator/agent identifier
|
|
51
52
|
*/
|
|
52
|
-
export function buildPhaseTransitionEvent({
|
|
53
|
+
export function buildPhaseTransitionEvent({
|
|
54
|
+
featureCode, from, to, outcome, agent_id, timestamp, origin, recordedAt, confidence,
|
|
55
|
+
}) {
|
|
53
56
|
const now = timestamp || new Date().toISOString();
|
|
54
57
|
const fromStr = from == null ? 'null' : String(from);
|
|
55
58
|
const id = phaseTransitionDecisionEventId(featureCode, from, to, now);
|
|
@@ -64,6 +67,9 @@ export function buildPhaseTransitionEvent({ featureCode, from, to, outcome, agen
|
|
|
64
67
|
metadata: {
|
|
65
68
|
from_phase: fromStr,
|
|
66
69
|
to_phase: String(to),
|
|
70
|
+
...(origin !== undefined ? { origin } : {}),
|
|
71
|
+
...(recordedAt !== undefined ? { recorded_at: recordedAt } : {}),
|
|
72
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
67
73
|
},
|
|
68
74
|
roles: [{ name: 'PRODUCER', agent_id: agent_id || null }],
|
|
69
75
|
};
|
|
@@ -173,3 +179,36 @@ export function buildGateEvent({ featureCode, gateLogEntryId, gateId, decision,
|
|
|
173
179
|
roles: [],
|
|
174
180
|
};
|
|
175
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build a kind=policy_violation DecisionEvent (COMP-POLICY-CHECK-5).
|
|
185
|
+
*
|
|
186
|
+
* CONTRACT: first-class kind since `contracts/comp-obs-contract.schema.json`
|
|
187
|
+
* v0.2.6 (2026-08-17) — closed metadata subschema {step_id, rule, matched,
|
|
188
|
+
* suppressed, user_mode, build_id}. Keep the emitted shape and the subschema in
|
|
189
|
+
* lockstep; extending either is a versioned contract edit.
|
|
190
|
+
*
|
|
191
|
+
* @param {{ featureCode, buildId, stepId, rule, matched, suppressed, userMode, timestamp }} params
|
|
192
|
+
*/
|
|
193
|
+
export function buildPolicyViolationEvent({
|
|
194
|
+
featureCode, buildId, stepId, rule, matched, suppressed, userMode, timestamp,
|
|
195
|
+
}) {
|
|
196
|
+
const now = timestamp || new Date().toISOString();
|
|
197
|
+
const id = policyViolationDecisionEventId(featureCode, buildId ?? 'no-build', stepId, rule, matched);
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
feature_code: featureCode,
|
|
201
|
+
timestamp: now,
|
|
202
|
+
kind: 'policy_violation',
|
|
203
|
+
title: `${suppressed ? 'Policy match suppressed' : 'Policy violation'}: ${rule}`,
|
|
204
|
+
metadata: {
|
|
205
|
+
step_id: stepId,
|
|
206
|
+
rule,
|
|
207
|
+
matched,
|
|
208
|
+
suppressed: Boolean(suppressed),
|
|
209
|
+
user_mode: userMode,
|
|
210
|
+
build_id: buildId ?? null,
|
|
211
|
+
},
|
|
212
|
+
roles: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -62,3 +62,20 @@ export function driftThresholdDecisionEventId(featureCode, axisId, breachStarted
|
|
|
62
62
|
const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
|
|
63
63
|
return uuidv5(`drift_threshold:${axisId}:${breachStartedAtIso}`, featureNs);
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Deterministic id for a policy_violation DecisionEvent (COMP-POLICY-CHECK-5).
|
|
68
|
+
* Unique per (featureCode, buildId, stepId, rule, matched pattern) so a replayed
|
|
69
|
+
* build-stream line re-derives the same id and the timeline stays deduplicated.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} featureCode
|
|
72
|
+
* @param {string} buildId
|
|
73
|
+
* @param {string} stepId
|
|
74
|
+
* @param {string} ruleName
|
|
75
|
+
* @param {string} matched — the pattern text that matched
|
|
76
|
+
* @returns {string} UUID v5
|
|
77
|
+
*/
|
|
78
|
+
export function policyViolationDecisionEventId(featureCode, buildId, stepId, ruleName, matched) {
|
|
79
|
+
const featureNs = uuidv5(String(featureCode), ROOT_NAMESPACE);
|
|
80
|
+
return uuidv5(`policy_violation:${buildId}:${stepId}:${ruleName}:${matched}`, featureNs);
|
|
81
|
+
}
|
|
@@ -53,6 +53,9 @@ export function deriveDecisionEvents(state, featureCode) {
|
|
|
53
53
|
outcome: entry.outcome,
|
|
54
54
|
agent_id: entry.agent_id || null,
|
|
55
55
|
timestamp: entry.timestamp,
|
|
56
|
+
origin: entry.origin,
|
|
57
|
+
recordedAt: entry.recordedAt,
|
|
58
|
+
confidence: entry.confidence,
|
|
56
59
|
}));
|
|
57
60
|
}
|
|
58
61
|
|
package/server/design-routes.js
CHANGED
|
@@ -16,7 +16,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
16
16
|
import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
|
|
17
17
|
import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
|
|
18
18
|
import { KNOWN_VERSIONS } from '../lib/build-stream-schema.js';
|
|
19
|
-
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
19
|
+
import { getTargetRoot, resolveProjectPath, trackProjectWork } from './project-root.js';
|
|
20
20
|
import { resolveStratumMcpConnection } from '../lib/stratum-engine.js';
|
|
21
21
|
import { relForDisplay } from '../lib/project-paths.js';
|
|
22
22
|
|
|
@@ -50,11 +50,11 @@ async function _getStratum(root = getTargetRoot(), { factory } = {}) {
|
|
|
50
50
|
// Test-only export: exercise the root-keyed connect/cache without a live server.
|
|
51
51
|
export { _getStratum as _getDesignStratumForTest };
|
|
52
52
|
|
|
53
|
-
/** Tear down
|
|
54
|
-
export async function closeDesignStratum() {
|
|
55
|
-
const
|
|
56
|
-
_stratumClients.
|
|
57
|
-
_stratumConnectPromises.
|
|
53
|
+
/** Tear down an evicted workspace's connection, or all connections at shutdown. */
|
|
54
|
+
export async function closeDesignStratum(projectRoot) {
|
|
55
|
+
const keys = projectRoot === undefined ? [..._stratumConnectPromises.keys()] : [projectRoot];
|
|
56
|
+
const clients = keys.map(key => _stratumClients.get(key)).filter(Boolean);
|
|
57
|
+
for (const key of keys) { _stratumClients.delete(key); _stratumConnectPromises.delete(key); }
|
|
58
58
|
for (const c of clients) {
|
|
59
59
|
try { await c.close(); } catch { /* ignore */ }
|
|
60
60
|
}
|
|
@@ -66,6 +66,12 @@ export const designListeners = new Map();
|
|
|
66
66
|
/** In-flight guard — prevents overlapping agent runs for the same session. */
|
|
67
67
|
const _inFlight = new Set();
|
|
68
68
|
|
|
69
|
+
export function hasDesignWork(projectRoot) {
|
|
70
|
+
const prefix = `${projectRoot}:`;
|
|
71
|
+
return [..._inFlight].some(key => key.startsWith(prefix))
|
|
72
|
+
|| [...designListeners].some(([key, clients]) => key.startsWith(prefix) && clients.size > 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
/**
|
|
70
76
|
* Build a session key for SSE listener scoping.
|
|
71
77
|
* @param {string} scope
|
|
@@ -384,7 +390,7 @@ export function attachDesignRoutes(app, { getSessionManager, getProjectRoot }) {
|
|
|
384
390
|
});
|
|
385
391
|
|
|
386
392
|
// POST /api/design/complete — generate design doc and mark session complete
|
|
387
|
-
app.post('/api/design/complete',
|
|
393
|
+
app.post('/api/design/complete', (req, res) => trackProjectWork(async () => {
|
|
388
394
|
const { scope, featureCode, draftDoc } = req.body || {};
|
|
389
395
|
try {
|
|
390
396
|
const sessionManager = getSessionManager();
|
|
@@ -528,7 +534,7 @@ Output ONLY the Markdown content, no code fences.`;
|
|
|
528
534
|
}
|
|
529
535
|
res.status(400).json({ error: err.message });
|
|
530
536
|
}
|
|
531
|
-
});
|
|
537
|
+
}));
|
|
532
538
|
|
|
533
539
|
// POST /api/design/revise — mark a decision as superseded and re-ask
|
|
534
540
|
app.post('/api/design/revise', (req, res) => {
|
package/server/feature-scan.js
CHANGED
|
@@ -18,6 +18,8 @@ import path from 'node:path';
|
|
|
18
18
|
import { parse as parseYaml } from 'yaml';
|
|
19
19
|
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
20
20
|
import { relForDisplay } from '../lib/project-paths.js';
|
|
21
|
+
import { featureStatusToVisionStatus } from '../lib/status-projection.js';
|
|
22
|
+
import { VERIFIED_BY } from './completion-projection.js';
|
|
21
23
|
import { assertValidLinkShape } from '../lib/feature-write-guard.js';
|
|
22
24
|
import { depsToEdges } from '../lib/roadmap-graph/model.js';
|
|
23
25
|
|
|
@@ -175,7 +177,9 @@ export function scanFeatures(featuresDir) {
|
|
|
175
177
|
const specPath = path.join(featureDir, 'feature.json');
|
|
176
178
|
if (fs.existsSync(specPath)) {
|
|
177
179
|
feature.hasFeatureJson = true;
|
|
180
|
+
feature.featureJsonUnreadable = true; // cleared once the parse succeeds
|
|
178
181
|
const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
|
|
182
|
+
feature.featureJsonUnreadable = false;
|
|
179
183
|
if (typeof spec.group === 'string' && spec.group.trim()) {
|
|
180
184
|
feature.group = spec.group.trim();
|
|
181
185
|
}
|
|
@@ -185,6 +189,7 @@ export function scanFeatures(featuresDir) {
|
|
|
185
189
|
// it (COMP-ROADMAP-GRAPH-2: kills the feature.json/cockpit status de-sync).
|
|
186
190
|
if (typeof spec.status === 'string' && spec.status.trim()) {
|
|
187
191
|
feature.status = normalizeStatus(spec.status);
|
|
192
|
+
feature.canonicalStatus = spec.status.trim().toUpperCase();
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
} catch { /* ignore malformed feature.json */ }
|
|
@@ -473,6 +478,33 @@ export function writeFeatureGroupToDisk(item, newGroup, featuresDir) {
|
|
|
473
478
|
* @param {object} store — VisionStore instance
|
|
474
479
|
* @returns {{ features: number, updated: number, connections: number }}
|
|
475
480
|
*/
|
|
481
|
+
/**
|
|
482
|
+
* The synchronous startup tier for a scanned `complete` feature (§2.3b).
|
|
483
|
+
* @returns {{status:string, completion_projection?:object}}
|
|
484
|
+
*/
|
|
485
|
+
function seedCompletionTier(feature) {
|
|
486
|
+
if (feature.canonicalStatus === 'COMPLETE') {
|
|
487
|
+
return {
|
|
488
|
+
status: 'complete',
|
|
489
|
+
completion_projection: { verified_by: VERIFIED_BY.CANONICAL, at: new Date().toISOString(), source: 'startup-scan' },
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (!feature.hasFeatureJson) {
|
|
493
|
+
// No feature.json at all: the scanner inferred completion from documents
|
|
494
|
+
// (report.md present, design.md prose). Weakest tier — and the ONLY case
|
|
495
|
+
// that gets it. This is what "unmanaged" means (§2.3b round 5).
|
|
496
|
+
return {
|
|
497
|
+
status: 'complete',
|
|
498
|
+
completion_projection: { verified_by: VERIFIED_BY.DOCUMENT, at: new Date().toISOString(), source: 'startup-scan' },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// A feature.json exists and does not say COMPLETE — malformed, or valid with
|
|
502
|
+
// no status while the documents claim completion. Canon wins: a managed
|
|
503
|
+
// feature is never seeded complete on document evidence (Codex r1 #4).
|
|
504
|
+
if (feature.featureJsonUnreadable) return { status: 'planned' };
|
|
505
|
+
return { status: featureStatusToVisionStatus(feature.canonicalStatus) || 'planned' };
|
|
506
|
+
}
|
|
507
|
+
|
|
476
508
|
export function seedFeatures(features, store) {
|
|
477
509
|
const seeded = { features: 0, updated: 0, connections: 0 };
|
|
478
510
|
const featureItemMap = new Map(); // featureCode → itemId
|
|
@@ -489,11 +521,23 @@ export function seedFeatures(features, store) {
|
|
|
489
521
|
);
|
|
490
522
|
|
|
491
523
|
if (!featureItem) {
|
|
524
|
+
// COMP-COMPLETION-GATE slice 3 (AC-4d, AC-16a, AC-16b): startup seeding is
|
|
525
|
+
// synchronous and pre-listen, so it does not consult the guard (§2.3b,
|
|
526
|
+
// round 5). A `complete` status is projected through the same predicate
|
|
527
|
+
// as every other transport — on canonical feature.json alone, stamped
|
|
528
|
+
// `canonical-status-only`, or `document-derived` for an unmanaged folder
|
|
529
|
+
// (no feature.json; the scanner inferred completion from report.md or doc
|
|
530
|
+
// metadata). That is display state, never a completion compose vouches
|
|
531
|
+
// for. A folder whose documents say complete but whose feature.json does
|
|
532
|
+
// NOT is created as its feature.json says.
|
|
533
|
+
const projected = feature.status === 'complete'
|
|
534
|
+
? seedCompletionTier(feature)
|
|
535
|
+
: { status: feature.status || 'planned' };
|
|
492
536
|
featureItem = store.createItem({
|
|
493
537
|
type: 'feature',
|
|
494
538
|
title: feature.name,
|
|
495
539
|
description: feature.description || '',
|
|
496
|
-
status:
|
|
540
|
+
status: projected.status,
|
|
497
541
|
phase: feature.phase || 'planning',
|
|
498
542
|
confidence: feature.confidence,
|
|
499
543
|
files: feature.artifacts.map(a => artifactPath(feature, a)),
|
|
@@ -503,6 +547,9 @@ export function seedFeatures(features, store) {
|
|
|
503
547
|
try {
|
|
504
548
|
store.updateLifecycle(featureItem.id, { featureCode: feature.name, currentPhase: 'explore_design' });
|
|
505
549
|
} catch { /* lifecycle method may not exist */ }
|
|
550
|
+
if (projected.completion_projection) {
|
|
551
|
+
store.updateItem(featureItem.id, { completion_projection: projected.completion_projection });
|
|
552
|
+
}
|
|
506
553
|
featureItem = store.items.get(featureItem.id);
|
|
507
554
|
seeded.features++;
|
|
508
555
|
} else {
|
|
@@ -511,8 +558,35 @@ export function seedFeatures(features, store) {
|
|
|
511
558
|
if (feature.description && feature.description !== featureItem.description) {
|
|
512
559
|
updates.description = feature.description;
|
|
513
560
|
}
|
|
561
|
+
// A complete item is RE-VERIFIED on every scan, not only when its status
|
|
562
|
+
// differs (Codex r1 #5, r2 #2): an item stamped when canon was valid must
|
|
563
|
+
// be downgraded if canon is later corrupted or loses its status, and an
|
|
564
|
+
// item complete before tiers existed must gain its stamp.
|
|
565
|
+
if (feature.status === 'complete' && featureItem.status === 'complete') {
|
|
566
|
+
const projected = seedCompletionTier(feature);
|
|
567
|
+
if (projected.status !== 'complete') {
|
|
568
|
+
updates.status = projected.status;
|
|
569
|
+
updates.completion_projection = null;
|
|
570
|
+
} else if (!featureItem.completion_projection
|
|
571
|
+
|| featureItem.completion_projection.verified_by !== projected.completion_projection.verified_by) {
|
|
572
|
+
updates.completion_projection = projected.completion_projection;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
514
575
|
if (feature.status && feature.status !== featureItem.status) {
|
|
515
|
-
|
|
576
|
+
if (feature.status === 'complete') {
|
|
577
|
+
const projected = seedCompletionTier(feature);
|
|
578
|
+
if (projected.status === 'complete') {
|
|
579
|
+
updates.status = 'complete';
|
|
580
|
+
updates.completion_projection = projected.completion_projection;
|
|
581
|
+
} else if (projected.status !== featureItem.status) {
|
|
582
|
+
updates.status = projected.status;
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
updates.status = feature.status;
|
|
586
|
+
// Leaving complete for any recognized status: the stamp is a claim
|
|
587
|
+
// about a completion that canon no longer records (Codex r3).
|
|
588
|
+
if (featureItem.completion_projection) updates.completion_projection = null;
|
|
589
|
+
}
|
|
516
590
|
}
|
|
517
591
|
if (feature.confidence > (featureItem.confidence || 0)) {
|
|
518
592
|
updates.confidence = feature.confidence;
|
package/server/file-watcher.js
CHANGED
|
@@ -10,9 +10,13 @@ import path from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
|
|
12
12
|
import { getTargetRoot, loadProjectConfig, ensureDataDir } from './project-root.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
resolveDocsPathFromConfig,
|
|
15
|
+
resolveFeaturesPathFromConfig,
|
|
16
|
+
resolveIdeaboxPathFromConfig,
|
|
17
|
+
relForDisplay,
|
|
18
|
+
} from '../lib/project-paths.js';
|
|
14
19
|
|
|
15
|
-
const PROJECT_ROOT = getTargetRoot();
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* fs.watch fileFilter for the pipelines/ watch (COMP-PIPE-EDIT-6): only
|
|
@@ -32,8 +36,77 @@ export function buildSpecChangedMessage(file, relativePath) {
|
|
|
32
36
|
return { type: 'specChanged', file, path: relativePath };
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/** Coalescing window for the ideabox-projection watch. */
|
|
40
|
+
export const IDEABOX_COALESCE_MS = 100;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* fs.watch fileFilter for the ideabox-projection watch (IDEA-24). That watch is
|
|
44
|
+
* NON-recursive on the projection's own parent directory, so `filename` is a bare
|
|
45
|
+
* name and exact equality is the whole test.
|
|
46
|
+
*
|
|
47
|
+
* Exact equality, not a suffix or basename match, because `render-ideabox.js`
|
|
48
|
+
* publishes atomically through `.ideabox.md.tmp.<uuid>` in the SAME directory. A
|
|
49
|
+
* looser predicate would fire on the temp file — announcing an update before the
|
|
50
|
+
* rename that makes it real, and a second time after.
|
|
51
|
+
*/
|
|
52
|
+
export function isIdeaboxProjectionFile(filename, projectionBasename) {
|
|
53
|
+
return typeof filename === 'string'
|
|
54
|
+
&& typeof projectionBasename === 'string'
|
|
55
|
+
&& projectionBasename.length > 0
|
|
56
|
+
&& filename === projectionBasename;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shape of the vision-WS `ideaboxUpdated` broadcast raised by the projection
|
|
61
|
+
* watch (IDEA-24).
|
|
62
|
+
*
|
|
63
|
+
* `type` is byte-identical to what `server/ideabox-routes.js` broadcasts, because
|
|
64
|
+
* both ideabox clients compare `msg.type === 'ideaboxUpdated'` and nothing else —
|
|
65
|
+
* a differently-named event would be silently ignored, which is the bug this
|
|
66
|
+
* closes. `timestamp` matches the route's shape so one channel carries one shape;
|
|
67
|
+
* `source` exists so a WS log distinguishes a route-driven update from a
|
|
68
|
+
* file-driven one, which is precisely the diagnosis that was missing here.
|
|
69
|
+
*/
|
|
70
|
+
export function buildIdeaboxUpdatedMessage(relativePath) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'ideaboxUpdated',
|
|
73
|
+
source: 'projection-watch',
|
|
74
|
+
path: relativePath,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fire `fn` once, `waitMs` after the LAST call. TRAILING, unlike the
|
|
81
|
+
* leading-edge-and-drop debounce the other watches use, and the difference is
|
|
82
|
+
* the whole point.
|
|
83
|
+
*
|
|
84
|
+
* A leading-edge debounce announces the first write of a burst and discards the
|
|
85
|
+
* rest — so the cockpit ends up showing the state at the START of the burst and
|
|
86
|
+
* never hears about the end of it. That is the same "your list is stale until
|
|
87
|
+
* you reload" bug IDEA-24 exists to close, just at a 100ms timescale, and it is
|
|
88
|
+
* reachable from two `compose ideabox add` calls in quick succession.
|
|
89
|
+
*
|
|
90
|
+
* For a NOTIFICATION the last event is the one that must survive; the payload
|
|
91
|
+
* is a re-fetch, so intermediate events carry nothing worth delivering. One
|
|
92
|
+
* broadcast per logical write, always reflecting final state, ~waitMs late.
|
|
93
|
+
*/
|
|
94
|
+
export function createTrailingDebouncer(fn, waitMs) {
|
|
95
|
+
let timer = null;
|
|
96
|
+
return {
|
|
97
|
+
trigger() {
|
|
98
|
+
if (timer) clearTimeout(timer);
|
|
99
|
+
timer = setTimeout(() => { timer = null; fn(); }, waitMs);
|
|
100
|
+
},
|
|
101
|
+
cancel() {
|
|
102
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
35
107
|
export class FileWatcherServer {
|
|
36
|
-
constructor() {
|
|
108
|
+
constructor({ projectRoot = getTargetRoot() } = {}) {
|
|
109
|
+
this.projectRoot = projectRoot;
|
|
37
110
|
this.clients = new Set();
|
|
38
111
|
this.wss = null;
|
|
39
112
|
this.watchers = [];
|
|
@@ -41,8 +114,8 @@ export class FileWatcherServer {
|
|
|
41
114
|
|
|
42
115
|
/** Resolve and validate a relative path stays within project root */
|
|
43
116
|
safePath(relativePath) {
|
|
44
|
-
const resolved = path.resolve(
|
|
45
|
-
if (!resolved.startsWith(
|
|
117
|
+
const resolved = path.resolve(this.projectRoot, relativePath);
|
|
118
|
+
if (!resolved.startsWith(this.projectRoot + path.sep) && resolved !== this.projectRoot) {
|
|
46
119
|
return null;
|
|
47
120
|
}
|
|
48
121
|
return resolved;
|
|
@@ -89,7 +162,7 @@ export class FileWatcherServer {
|
|
|
89
162
|
const config = loadProjectConfig();
|
|
90
163
|
const docsPrefix = config.paths?.docs || 'docs';
|
|
91
164
|
// COMP-PATHS-EXTERNAL: list the RESOLVED docs dir (may be relocated).
|
|
92
|
-
const docsDir = resolveDocsPathFromConfig(
|
|
165
|
+
const docsDir = resolveDocsPathFromConfig(this.projectRoot, config);
|
|
93
166
|
try {
|
|
94
167
|
const files = this.listMarkdownFiles(docsDir, docsPrefix);
|
|
95
168
|
res.json({ files });
|
|
@@ -174,26 +247,44 @@ export class FileWatcherServer {
|
|
|
174
247
|
startWatching() {
|
|
175
248
|
const debounceMap = new Map();
|
|
176
249
|
|
|
177
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} [opts]
|
|
252
|
+
* @param {boolean} [opts.recursive=true] fs.watch recursion.
|
|
253
|
+
* @param {number} [opts.debounceMs=100] leading-edge suppression window.
|
|
254
|
+
*
|
|
255
|
+
* PASS 0 FOR ANY WATCH OVER AN ALREADY-WATCHED FILE. `debounceMap` is
|
|
256
|
+
* shared across every watchDir call and keyed by the prefixed relative
|
|
257
|
+
* path, so two watches whose dir+prefix resolve to the SAME relative path
|
|
258
|
+
* for one file suppress each other: whichever fs.watch delivers second
|
|
259
|
+
* inside the window is silently dropped, and which one that is depends on
|
|
260
|
+
* the OS. (That already happens between `docs/` and `docs/features/`; it is
|
|
261
|
+
* left exactly as it was rather than changed underneath callers here.) 0
|
|
262
|
+
* opts out of the shared map entirely — for a watch that coalesces its own
|
|
263
|
+
* events, which is the only reason to be in that position.
|
|
264
|
+
*/
|
|
265
|
+
const watchDir = (dir, prefix, onChanged, fileFilter = (f) => f.endsWith('.md'), opts = {}) => {
|
|
266
|
+
const { recursive = true, debounceMs = 100 } = opts;
|
|
178
267
|
if (!fs.existsSync(dir)) {
|
|
179
268
|
console.warn(`[file-watcher] ${prefix}/ directory not found, skipping watch`);
|
|
180
269
|
return;
|
|
181
270
|
}
|
|
182
271
|
try {
|
|
183
|
-
const watcher = fs.watch(dir, { recursive
|
|
272
|
+
const watcher = fs.watch(dir, { recursive }, (eventType, filename) => {
|
|
184
273
|
if (!filename || !fileFilter(filename)) return;
|
|
185
274
|
|
|
186
275
|
const relativePath = path.join(prefix, filename);
|
|
187
276
|
// COMP-PATHS-EXTERNAL: derive the real path from the WATCHED dir, not
|
|
188
|
-
// by re-rooting under
|
|
277
|
+
// by re-rooting under this.projectRoot — the dir may be relocated outside
|
|
189
278
|
// the workspace. Byte-identical to the old form for an in-root dir.
|
|
190
279
|
const fullPath = path.join(dir, filename);
|
|
191
280
|
|
|
192
|
-
// Debounce: ignore events within
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
281
|
+
// Debounce: ignore events within debounceMs of each other for the same file
|
|
282
|
+
if (debounceMs > 0) {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const lastEvent = debounceMap.get(relativePath);
|
|
285
|
+
if (lastEvent && now - lastEvent < debounceMs) return;
|
|
286
|
+
debounceMap.set(relativePath, now);
|
|
287
|
+
}
|
|
197
288
|
|
|
198
289
|
onChanged(relativePath, fullPath);
|
|
199
290
|
});
|
|
@@ -204,10 +295,10 @@ export class FileWatcherServer {
|
|
|
204
295
|
};
|
|
205
296
|
|
|
206
297
|
// Watch docs/ — broadcast fileChanged events. COMP-PATHS-EXTERNAL: watch
|
|
207
|
-
// the RESOLVED absolute dir (may be relocated outside
|
|
298
|
+
// the RESOLVED absolute dir (may be relocated outside this.projectRoot).
|
|
208
299
|
const config = loadProjectConfig();
|
|
209
300
|
const docsPrefix = config.paths?.docs || 'docs';
|
|
210
|
-
watchDir(resolveDocsPathFromConfig(
|
|
301
|
+
watchDir(resolveDocsPathFromConfig(this.projectRoot, config), docsPrefix, (relativePath, fullPath) => {
|
|
211
302
|
try {
|
|
212
303
|
if (!fs.existsSync(fullPath)) return;
|
|
213
304
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -219,9 +310,9 @@ export class FileWatcherServer {
|
|
|
219
310
|
|
|
220
311
|
// Watch features/ — notify for auto-reseed into vision store
|
|
221
312
|
const featuresPrefix = config.paths?.features || 'docs/features';
|
|
222
|
-
watchDir(resolveFeaturesPathFromConfig(
|
|
313
|
+
watchDir(resolveFeaturesPathFromConfig(this.projectRoot, config), featuresPrefix, (relativePath, fullPath) => {
|
|
223
314
|
// Also broadcast as fileChanged (features are docs). fullPath comes from
|
|
224
|
-
// the watched dir (COMP-PATHS-EXTERNAL) — do not re-root under
|
|
315
|
+
// the watched dir (COMP-PATHS-EXTERNAL) — do not re-root under this.projectRoot.
|
|
225
316
|
try {
|
|
226
317
|
if (fs.existsSync(fullPath)) {
|
|
227
318
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -241,16 +332,67 @@ export class FileWatcherServer {
|
|
|
241
332
|
// `fileChanged` on this server's /ws/files. The payload carries `file` as a
|
|
242
333
|
// BASENAME because editorSpecFile is a bare filename (a prefixed relative path
|
|
243
334
|
// would never match the store's compare).
|
|
244
|
-
const pipelinesDir = path.join(
|
|
335
|
+
const pipelinesDir = path.join(this.projectRoot, 'pipelines');
|
|
245
336
|
watchDir(pipelinesDir, 'pipelines', (relativePath, fullPath) => {
|
|
246
337
|
if (typeof this.onSpecChanged === 'function') {
|
|
247
338
|
this.onSpecChanged(buildSpecChangedMessage(path.basename(fullPath), relativePath));
|
|
248
339
|
}
|
|
249
340
|
}, isStratumSpecFile);
|
|
250
341
|
|
|
342
|
+
// Watch the ideabox projection → `ideaboxUpdated` on the VISION WS (IDEA-24).
|
|
343
|
+
//
|
|
344
|
+
// A CLI ideabox write goes straight to the record store; it never reaches
|
|
345
|
+
// `server/ideabox-routes.js`, which is the only thing that broadcasts. So an
|
|
346
|
+
// open cockpit or mobile client kept showing the pre-write list until someone
|
|
347
|
+
// reloaded it by hand. The `fileChanged` this file already emits for the same
|
|
348
|
+
// write goes out on /ws/files, which neither ideabox client subscribes to.
|
|
349
|
+
//
|
|
350
|
+
// THE PROJECTION IS THE TRIGGER, NOT THE PAYLOAD. Clients respond by
|
|
351
|
+
// re-fetching GET /api/ideabox, which reads RECORDS — the markdown is never
|
|
352
|
+
// parsed to serve a read (COMP-PLAN-IDEA-UNIFY D21). The projection is used
|
|
353
|
+
// only as the signal, and it is a sound one because `ideabox-ops.js`
|
|
354
|
+
// guarantees the record is durable BEFORE the render: an event from this
|
|
355
|
+
// watch can never arrive ahead of the data the re-fetch will return.
|
|
356
|
+
//
|
|
357
|
+
// Two consequences, both accepted:
|
|
358
|
+
// - An API-driven mutation broadcasts twice (the route's own, then this
|
|
359
|
+
// one). The re-fetch is idempotent, and suppressing the echo would need a
|
|
360
|
+
// "did I just write this?" mtime handshake — a race, to save one GET.
|
|
361
|
+
// - A write whose render FAILED does not notify. That is the
|
|
362
|
+
// `projectionStale` path: the writer is already told, and `compose ideabox
|
|
363
|
+
// render` both repairs the file and fires this watch.
|
|
364
|
+
//
|
|
365
|
+
// Watched NON-recursively on the projection's own parent so a relocated
|
|
366
|
+
// `paths.ideabox` outside `paths.docs` still works.
|
|
367
|
+
const ideaboxPath = resolveIdeaboxPathFromConfig(this.projectRoot, config);
|
|
368
|
+
const ideaboxDir = path.dirname(ideaboxPath);
|
|
369
|
+
// The projection's directory may not exist yet in a project that has never
|
|
370
|
+
// rendered one. Create it, or the watch is skipped and the very first CLI
|
|
371
|
+
// write — the one most likely to be watched for — silently does not refresh.
|
|
372
|
+
try { fs.mkdirSync(ideaboxDir, { recursive: true }); } catch { /* read-only tree — watchDir skips */ }
|
|
373
|
+
//
|
|
374
|
+
// Raw fs events are coalesced by a TRAILING debouncer rather than the
|
|
375
|
+
// leading-edge one `watchDir` applies (hence `debounceMs: 0`): see
|
|
376
|
+
// `createTrailingDebouncer` for why the LAST event is the one that has to
|
|
377
|
+
// survive here. That also folds the rename+change pair of a single atomic
|
|
378
|
+
// publish into one broadcast.
|
|
379
|
+
const ideaboxRelative = relForDisplay(this.projectRoot, ideaboxPath);
|
|
380
|
+
this._ideaboxDebouncer = createTrailingDebouncer(() => {
|
|
381
|
+
if (typeof this.onIdeaboxChanged === 'function') {
|
|
382
|
+
this.onIdeaboxChanged(buildIdeaboxUpdatedMessage(ideaboxRelative));
|
|
383
|
+
}
|
|
384
|
+
}, IDEABOX_COALESCE_MS);
|
|
385
|
+
watchDir(
|
|
386
|
+
ideaboxDir,
|
|
387
|
+
relForDisplay(this.projectRoot, ideaboxDir),
|
|
388
|
+
() => this._ideaboxDebouncer.trigger(),
|
|
389
|
+
(f) => isIdeaboxProjectionFile(f, path.basename(ideaboxPath)),
|
|
390
|
+
{ recursive: false, debounceMs: 0 },
|
|
391
|
+
);
|
|
392
|
+
|
|
251
393
|
// Watch .compose/data/ for active-build.json changes
|
|
252
394
|
const self = this;
|
|
253
|
-
const dataDir = path.join(
|
|
395
|
+
const dataDir = path.join(this.projectRoot, '.compose', 'data');
|
|
254
396
|
let dataDirWatcherRegistered = false;
|
|
255
397
|
|
|
256
398
|
// Guarantee .compose/data/ exists before registering the watcher
|
|
@@ -309,11 +451,18 @@ export class FileWatcherServer {
|
|
|
309
451
|
return results;
|
|
310
452
|
}
|
|
311
453
|
|
|
312
|
-
|
|
454
|
+
stopWatching() {
|
|
313
455
|
for (const watcher of this.watchers) {
|
|
314
456
|
watcher.close();
|
|
315
457
|
}
|
|
458
|
+
// AFTER the watchers: cancelling first would leave a window in which a last
|
|
459
|
+
// fs event re-arms the timer, and a pending timer holds the event loop open.
|
|
460
|
+
this._ideaboxDebouncer?.cancel();
|
|
316
461
|
this.watchers = [];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
close() {
|
|
465
|
+
this.stopWatching();
|
|
317
466
|
for (const client of this.clients) {
|
|
318
467
|
client.close();
|
|
319
468
|
}
|