@smartmemory/compose 0.2.56-beta → 0.2.58-beta
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/contracts/feature-json.schema.json +16 -0
- package/dist/assets/{App-B4vuNX1g.js → App-s4CulNkO.js} +240 -240
- package/dist/assets/{abnfDiagram-VRR7QNED-B8gEiWHf.js → abnfDiagram-VRR7QNED-CDwzQ5wr.js} +1 -1
- package/dist/assets/{arc-Bm5kTLjD.js → arc-kIU8DDth.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-CBaqdB_L.js → architectureDiagram-ZJ3FMSHR-CleorO_0.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-RfS1bm8E.js → blockDiagram-677ZJIJ3-NvEFfoMk.js} +1 -1
- package/dist/assets/{browser-Cr0recrN.js → browser-CvETNQNv.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BFv92TMM.js → c4Diagram-LMCZKHZV-BcDQdTVB.js} +1 -1
- package/dist/assets/channel-DiJkE9og.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-B2XD65Ql.js → chunk-2Q5K7J3B-BseRkpm4.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-CLJH-LjM.js → chunk-32BRIVSS-DCHhvgFV.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BkOdBdcj.js → chunk-5VM5RSS4-DcTpMdJ4.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BY9ywAYh.js → chunk-EX3LRPZG-BdN8fvWs.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CBBIoS07.js → chunk-JWPE2WC7-DmZJP-8E.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-BZFkgaBr.js → chunk-MOJQB5TN-V74uYM17.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-C0ZEmfqx.js → chunk-RYQCIY6F-CoXUXbn_.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CKNWOjmw.js → chunk-V7JOEXUC-DUscIydR.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Bx3WjdJ3.js → chunk-VR4S4FIN-C4UDHq_a.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Ct9K5k9G.js → chunk-XXDRQBXY-DIH9i8mZ.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-nz4ydSEO.js → cose-bilkent-JH36ORCC-Dn6Gy6l1.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DID2ucPE.js → cynefin-VYW2F7L2-C2LW5fzB.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-YbvuXwBB.js → cynefinDiagram-TSTJHNR4-CGoAq7C9.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-DxTA5-If.js → dagre-VKFMJZFB-BaOKtuBE.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BIaCD_qo.js → diagram-FQU43EPY-BdQwqt4x.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-DK_qkNJs.js → diagram-G47NLZAW-Dkcf0o5z.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CrJVSMoz.js → diagram-NH7WQ7WH-C_fDi59r.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-Bm7HIxZi.js → diagram-OA4YK3LP-jaTQcOxt.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-Bu6nbQK2.js → diagram-WEI45ONY-dEtqVj3s.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-B7mnwRX5.js → ebnfDiagram-CCIWWBDH-s8ImqsSs.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-BRAfRVRu.js → erDiagram-Q63AITRT-5Zy29-SA.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-DGbxzKqF.js → flowDiagram-23GEKE2U-DV74V9he.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-CW_0kejm.js → ganttDiagram-NO4QXBWP-BND-CiLO.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-Bmy0T13a.js → gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js} +1 -1
- package/dist/assets/{graph-xkel59g2.js → graph--wGAvskB.js} +1 -1
- package/dist/assets/{index-D4TlupB7.js → index-CBWbmG7W.js} +3 -3
- package/dist/assets/{infoDiagram-FWYZ7A6U-CWPUMg-E.js → infoDiagram-FWYZ7A6U-Duyz8rZM.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-DkNHdDsm.js → ishikawaDiagram-FXEZZL3T-D2Qft0e5.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-CEzDprBt.js → journeyDiagram-5HDEW3XC-q6rG4Rig.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-BfY8Rvep.js → kanban-definition-HUTT4EX6-CsPX7gBL.js} +1 -1
- package/dist/assets/{linear-DbaFz_Eo.js → linear-CiccpAAd.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-DPep1uhn.js → mindmap-definition-LN4V7U3C-DRW2njpA.js} +1 -1
- package/dist/assets/mobile-PV1zQVwv.js +17 -0
- package/dist/assets/{pegDiagram-2B236MQR-D3ucuvxJ.js → pegDiagram-2B236MQR-DoDgm2S_.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-BP6PrvRT.js → pieDiagram-ENE6RG2P-CNSYLVAF.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-DqL3BMFc.js → quadrantDiagram-ABIIQ3AL-8-Nd6ony.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-UGGWX3B7.js → railroadDiagram-RFXS5EU6-bfdg6Bo4.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-BwtWKMbS.js → requirementDiagram-TGXJPOKE-slDkxCQy.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-CHNubde1.js → sankeyDiagram-HTMAVEWB-CR3CboiK.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-DmXC79Eq.js → sequenceDiagram-DBY2YBRQ-BIXw1JS1.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-B_tckiVY.js → sizeCapture-X5ZJPWSS-BdKI7JyF.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-CU-NWaBE.js → stateDiagram-2N3HPSRC-ktVgpBZS.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-D5uOTtln.js → swimlanes-5IMT3BWC-CaXpC3FM.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-lfUZ73z6.js → timeline-definition-FHXFAJF6-CKWSlRUl.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-DpsKaW7a.js → vennDiagram-L72KCM5P-CwOhxDyv.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DF9TcdSf.js → wardleyDiagram-EHGQE667-DGavMpEB.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-DrO8j_TX.js → xychartDiagram-FW5EYKEG-CWmnmLZz.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/build.js +46 -31
- package/lib/escalation.js +69 -0
- package/lib/feature-writer.js +82 -8
- package/lib/lane-gate.js +200 -0
- package/lib/triage.js +144 -0
- package/package.json +1 -1
- package/server/settings-routes.js +11 -4
- package/server/settings-store.js +53 -5
- package/dist/assets/channel-BwltV6wa.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-D4b7dksL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-D4b7dksL.js +0 -1
- package/dist/assets/mobile-CNLMhdFP.js +0 -17
- package/dist/assets/stateDiagram-v2-6OUMAXLB-C-St-BDG.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-B5KMjp5z.js +0 -8
package/lib/feature-writer.js
CHANGED
|
@@ -63,6 +63,52 @@ const COMPLEXITIES = new Set(['S', 'M', 'L', 'XL']);
|
|
|
63
63
|
// produced feature.json. Same vocab the ideabox uses (low|medium|high).
|
|
64
64
|
const IMPACTS = new Set(['low', 'medium', 'high']);
|
|
65
65
|
|
|
66
|
+
// COMP-TRIAGE-5: front-of-pipeline scope-estimation vocab. `lane` drives which
|
|
67
|
+
// phases get skipped; `estimateSource` records whether the lane came from the
|
|
68
|
+
// front estimator, the doc-reading refinement, or an escalation bump.
|
|
69
|
+
const LANES = new Set(['trivial', 'standard', 'complex']);
|
|
70
|
+
const ESTIMATE_SOURCES = new Set(['front', 'refined', 'escalated']);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// validateFeatureFields — shared field-shape guard
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate the subset of feature.json fields that carry a fixed vocabulary or
|
|
78
|
+
* range. Called from both addRoadmapEntry (below) and the build write path
|
|
79
|
+
* (lib/build.js) so neither can bypass the guard the way the old inline
|
|
80
|
+
* `COMPLEXITIES.has` check — scoped only to addRoadmapEntry — allowed
|
|
81
|
+
* `complexity: String(triageResult.tier)` (e.g. "0".."4") to reach disk via a
|
|
82
|
+
* raw provider write.
|
|
83
|
+
*
|
|
84
|
+
* Every field is optional; only fields present on `fields` are checked.
|
|
85
|
+
* Throws a plain Error (matching addRoadmapEntry's existing signaling) on the
|
|
86
|
+
* first violation found.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} fields
|
|
89
|
+
* @param {string} [fields.complexity] one of S|M|L|XL
|
|
90
|
+
* @param {number} [fields.triageTier] integer 0-4
|
|
91
|
+
* @param {string} [fields.lane] one of trivial|standard|complex
|
|
92
|
+
* @param {string} [fields.estimateSource] one of front|refined|escalated
|
|
93
|
+
*/
|
|
94
|
+
export function validateFeatureFields(fields) {
|
|
95
|
+
if (fields.complexity !== undefined && !COMPLEXITIES.has(fields.complexity)) {
|
|
96
|
+
throw new Error(`feature-writer: invalid complexity "${fields.complexity}"`);
|
|
97
|
+
}
|
|
98
|
+
if (fields.triageTier !== undefined &&
|
|
99
|
+
(!Number.isInteger(fields.triageTier) || fields.triageTier < 0 || fields.triageTier > 4)) {
|
|
100
|
+
throw new Error(`feature-writer: invalid triageTier "${fields.triageTier}" — must be an integer 0-4`);
|
|
101
|
+
}
|
|
102
|
+
if (fields.lane !== undefined && !LANES.has(fields.lane)) {
|
|
103
|
+
throw new Error(`feature-writer: invalid lane "${fields.lane}" — must be one of ${[...LANES].join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
if (fields.estimateSource !== undefined && !ESTIMATE_SOURCES.has(fields.estimateSource)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`feature-writer: invalid estimateSource "${fields.estimateSource}" — must be one of ${[...ESTIMATE_SOURCES].join(', ')}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
// ---------------------------------------------------------------------------
|
|
67
113
|
// Helpers
|
|
68
114
|
// ---------------------------------------------------------------------------
|
|
@@ -75,11 +121,25 @@ function validateCode(code) {
|
|
|
75
121
|
}
|
|
76
122
|
}
|
|
77
123
|
|
|
124
|
+
// AUDIT-19: a writer result must never carry the full regenerated ROADMAP.
|
|
125
|
+
// roundtripGuard now returns only small diagnostic fields, but a persistent
|
|
126
|
+
// idempotency-cache entry written before that change can still hold
|
|
127
|
+
// roundtrip.canonical (the entire markdown), so re-sanitize on the way out —
|
|
128
|
+
// covering both the fresh and cache-hit paths, and any future roundtrip writer.
|
|
129
|
+
// No-op for results that don't carry roundtrip.canonical.
|
|
130
|
+
function sanitizeWriterResult(result) {
|
|
131
|
+
if (result && result.roundtrip && typeof result.roundtrip === 'object' && 'canonical' in result.roundtrip) {
|
|
132
|
+
const { canonical, ...safe } = result.roundtrip;
|
|
133
|
+
return { ...result, roundtrip: safe };
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
78
138
|
function maybeIdempotent(args, fn) {
|
|
79
139
|
if (args.idempotency_key) {
|
|
80
|
-
return checkOrInsert(args.cwd, args.idempotency_key, fn).then(({ result }) => result);
|
|
140
|
+
return checkOrInsert(args.cwd, args.idempotency_key, fn).then(({ result }) => sanitizeWriterResult(result));
|
|
81
141
|
}
|
|
82
|
-
return Promise.resolve().then(fn);
|
|
142
|
+
return Promise.resolve().then(fn).then(sanitizeWriterResult);
|
|
83
143
|
}
|
|
84
144
|
|
|
85
145
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +161,9 @@ function maybeIdempotent(args, fn) {
|
|
|
101
161
|
* @param {string} [args.triageTimestamp] COMP-ROADMAP-PLAN: triage cache stamp
|
|
102
162
|
* @param {string} [args.plannedBy] COMP-ROADMAP-PLAN: originating plan session
|
|
103
163
|
* @param {string} [args.impact] COMP-ROADMAP-PLAN: low | medium | high
|
|
164
|
+
* @param {string} [args.lane] COMP-TRIAGE-5: trivial | standard | complex
|
|
165
|
+
* @param {number} [args.triageTier] COMP-TRIAGE-5: integer 0-4
|
|
166
|
+
* @param {string} [args.estimateSource] COMP-TRIAGE-5: front | refined | escalated
|
|
104
167
|
* @param {boolean} [args.force]
|
|
105
168
|
* @param {string} [args.idempotency_key]
|
|
106
169
|
*/
|
|
@@ -113,9 +176,11 @@ export async function addRoadmapEntry(cwd, args) {
|
|
|
113
176
|
validateCode(args.code);
|
|
114
177
|
if (!args.description) throw new Error('feature-writer: description is required');
|
|
115
178
|
if (!args.phase) throw new Error('feature-writer: phase is required');
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
// COMP-TRIAGE-5: complexity/triageTier/lane/estimateSource shape validation
|
|
180
|
+
// lives in the shared validateFeatureFields guard so the build write path
|
|
181
|
+
// (lib/build.js) can call the same check and can't bypass it the way the
|
|
182
|
+
// old inline `COMPLEXITIES.has` (scoped only to this function) allowed.
|
|
183
|
+
validateFeatureFields(args);
|
|
119
184
|
const status = args.status ?? 'PLANNED';
|
|
120
185
|
if (!STATUSES.has(status)) {
|
|
121
186
|
throw new Error(`feature-writer: invalid status "${status}"`);
|
|
@@ -166,6 +231,10 @@ export async function addRoadmapEntry(cwd, args) {
|
|
|
166
231
|
if (args.triageTimestamp !== undefined) feature.triageTimestamp = args.triageTimestamp;
|
|
167
232
|
if (args.plannedBy !== undefined) feature.plannedBy = args.plannedBy;
|
|
168
233
|
if (args.impact !== undefined) feature.impact = args.impact;
|
|
234
|
+
// COMP-TRIAGE-5: front-of-pipeline scope-estimation fields.
|
|
235
|
+
if (args.lane !== undefined) feature.lane = args.lane;
|
|
236
|
+
if (args.triageTier !== undefined) feature.triageTier = args.triageTier;
|
|
237
|
+
if (args.estimateSource !== undefined) feature.estimateSource = args.estimateSource;
|
|
169
238
|
|
|
170
239
|
let roundtrip = null;
|
|
171
240
|
if (isLocalProvider(provider)) {
|
|
@@ -263,8 +332,8 @@ function readRoadmapBase(roadmapPath) {
|
|
|
263
332
|
//
|
|
264
333
|
// Blocks only on fixed-point divergence (a visibly churning rendered view);
|
|
265
334
|
// losslessness is surfaced by the validator (Task 6 / validate_project), not
|
|
266
|
-
// blocked here.
|
|
267
|
-
//
|
|
335
|
+
// blocked here. Return only the small diagnostic fields; `canonical` is the full
|
|
336
|
+
// regenerated ROADMAP and must not leak into MCP writer results.
|
|
268
337
|
async function roundtripGuard(cwd, provider, mutate, { force, label }) {
|
|
269
338
|
const current = await provider.listFeatures();
|
|
270
339
|
const projected = mutate(current.map(f => ({ ...f })));
|
|
@@ -286,7 +355,12 @@ async function roundtripGuard(cwd, provider, mutate, { force, label }) {
|
|
|
286
355
|
err.code = 'ROUNDTRIP_NOT_FIXED_POINT';
|
|
287
356
|
throw err;
|
|
288
357
|
}
|
|
289
|
-
return
|
|
358
|
+
return {
|
|
359
|
+
fixedPoint: rt.fixedPoint,
|
|
360
|
+
lossless: rt.lossless,
|
|
361
|
+
passes: rt.passes,
|
|
362
|
+
diffs: rt.diffs,
|
|
363
|
+
};
|
|
290
364
|
}
|
|
291
365
|
|
|
292
366
|
// Audit-log writes are best-effort: a failed append must NOT roll back a
|
package/lib/lane-gate.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// lib/lane-gate.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-TRIAGE-5 — E3 (Estimate → Execute → Expand) front-of-pipeline scope
|
|
4
|
+
// estimation + verification-gated escalation. Two entry points, extracted from
|
|
5
|
+
// build.js runBuild/executeShipStep so they are unit-testable in isolation:
|
|
6
|
+
//
|
|
7
|
+
// applyFrontTriage() — E3 "Estimate": derive the lane from the RAW REQUEST
|
|
8
|
+
// before any design/plan/blueprint doc is read, and
|
|
9
|
+
// persist validated feature fields (closing the
|
|
10
|
+
// complexity: String(tier) bypass at build.js:997/1006).
|
|
11
|
+
//
|
|
12
|
+
// maybeEscalateLane() — E3 "Expand": on a failed ship-time test gate, escalate
|
|
13
|
+
// the lane so the NEXT build runs wider — persist the
|
|
14
|
+
// escalated lane + a widened profile + a bounded counter
|
|
15
|
+
// and drop a resume checkpoint. STOP → human handoff.
|
|
16
|
+
// Re-entry is via re-invocation reading the escalated
|
|
17
|
+
// lane, NOT inline runBuild surgery.
|
|
18
|
+
|
|
19
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
import {
|
|
22
|
+
estimateScope,
|
|
23
|
+
tierToComplexity,
|
|
24
|
+
floorProfileToLane,
|
|
25
|
+
runTriage,
|
|
26
|
+
tierToLane,
|
|
27
|
+
narrowerLane,
|
|
28
|
+
} from './triage.js';
|
|
29
|
+
import { validateFeatureFields } from './feature-writer.js';
|
|
30
|
+
import { escalate } from './escalation.js';
|
|
31
|
+
|
|
32
|
+
// Docs that make a doc-reading refinement meaningful. Absent all three, runTriage
|
|
33
|
+
// would see zero files and return tier 0 ('trivial') — which is "no signal", not
|
|
34
|
+
// "confirmed simple", and must NOT be allowed to narrow away the front safety clamp.
|
|
35
|
+
const REFINEMENT_DOCS = ['design.md', 'plan.md', 'blueprint.md'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* E3 Estimate. Runs before spec load. Derives {tier, profile, lane} from the raw
|
|
39
|
+
* request (doc-free) and persists validated fields through the shared validator
|
|
40
|
+
* (so the tier can no longer be stringified into `complexity`). Returns the
|
|
41
|
+
* values runBuild needs to toggle skip_if.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} a
|
|
44
|
+
* @param {string} a.featureCode
|
|
45
|
+
* @param {string} [a.request] raw task text (opts.description); falls back to the code
|
|
46
|
+
* @param {object} a.provider build provider (getFeature/createFeature/putFeature)
|
|
47
|
+
* @param {object|null} a.cachedFeature already-fetched feature.json, or null
|
|
48
|
+
* @returns {Promise<{buildProfile:object, tier:number, lane:string, tierLabel:string, rationale:string, cachedFeature:object}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function applyFrontTriage({ featureCode, request, provider, cachedFeature, cwd, featuresDir }) {
|
|
51
|
+
// The CLI build path does not thread a description into opts, so fall back to
|
|
52
|
+
// the feature's own persisted description before the (signal-free) code string —
|
|
53
|
+
// otherwise every CLI build would classify the code and land low-confidence.
|
|
54
|
+
const req = request ?? cachedFeature?.description ?? featureCode;
|
|
55
|
+
const front = estimateScope(req);
|
|
56
|
+
let tier = front.tier;
|
|
57
|
+
let lane = front.lane;
|
|
58
|
+
let buildProfile = front.profile;
|
|
59
|
+
let estimateSource = 'front';
|
|
60
|
+
|
|
61
|
+
// E3 refinement (narrow-only): once design/plan/blueprint docs exist, a
|
|
62
|
+
// doc-reading pass may CONFIRM a smaller scope than the raw request implied —
|
|
63
|
+
// but it may only narrow the front lane, never widen it (widening is the
|
|
64
|
+
// escalation path's job). Gated on doc existence so a doc-less tier-0 read
|
|
65
|
+
// cannot undo the front safety clamp.
|
|
66
|
+
if (cwd && featuresDir) {
|
|
67
|
+
const dir = resolve(cwd, featuresDir, featureCode);
|
|
68
|
+
const hasDocs = REFINEMENT_DOCS.some((f) => existsSync(join(dir, f)));
|
|
69
|
+
if (hasDocs) {
|
|
70
|
+
try {
|
|
71
|
+
const refined = await runTriage(featureCode, { cwd, featuresDir });
|
|
72
|
+
const narrowed = narrowerLane(lane, tierToLane(refined.tier));
|
|
73
|
+
if (narrowed !== lane) {
|
|
74
|
+
lane = narrowed;
|
|
75
|
+
tier = refined.tier;
|
|
76
|
+
buildProfile = floorProfileToLane(refined.profile, narrowed);
|
|
77
|
+
estimateSource = 'refined';
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* triage read failed — keep the front estimate */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fields = {
|
|
86
|
+
complexity: tierToComplexity(tier),
|
|
87
|
+
triageTier: tier,
|
|
88
|
+
lane,
|
|
89
|
+
estimateSource,
|
|
90
|
+
profile: buildProfile,
|
|
91
|
+
triageTimestamp: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
// Throws on any invalid field — this is the guard the raw provider write bypassed.
|
|
94
|
+
validateFeatureFields(fields);
|
|
95
|
+
|
|
96
|
+
let updated;
|
|
97
|
+
if (!cachedFeature) {
|
|
98
|
+
updated = await provider.createFeature(featureCode, {
|
|
99
|
+
code: featureCode,
|
|
100
|
+
description: req,
|
|
101
|
+
status: 'PLANNED',
|
|
102
|
+
...fields,
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
updated = await provider.putFeature(featureCode, { ...cachedFeature, ...fields });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
buildProfile,
|
|
110
|
+
tier,
|
|
111
|
+
lane,
|
|
112
|
+
tierLabel: fields.complexity,
|
|
113
|
+
rationale: front.rationale,
|
|
114
|
+
cachedFeature: updated,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* E3 Expand. On a failed ship-time test gate, escalate the lane so the next build
|
|
120
|
+
* runs wider. Bounded via escalationCount; on STOP or exhausted ladder, writes a
|
|
121
|
+
* checkpoint and hands off to a human. Best-effort — the caller never blocks ship
|
|
122
|
+
* on this. Only acts on features that went through front triage (have a `lane`).
|
|
123
|
+
*
|
|
124
|
+
* @param {object} a
|
|
125
|
+
* @param {string} a.featureCode
|
|
126
|
+
* @param {object} a.provider build provider
|
|
127
|
+
* @param {string} [a.featureDir] dir to write the escalation checkpoint into
|
|
128
|
+
* @param {string|null} [a.currentPhase] optional live phase (unused for reEntry today; see escalation.js)
|
|
129
|
+
* @returns {Promise<{action:'none'|'escalate'|'stop', [from]:string, [to]:string, [reEntryPhase]:string, [escalationCount]:number}>}
|
|
130
|
+
*/
|
|
131
|
+
export async function maybeEscalateLane({ featureCode, provider, featureDir, currentPhase = null }) {
|
|
132
|
+
const feature = await provider.getFeature(featureCode);
|
|
133
|
+
// Only features that went through the front seam carry a lane — never escalate
|
|
134
|
+
// a feature that never opted into lane-based execution.
|
|
135
|
+
if (!feature?.lane) return { action: 'none' };
|
|
136
|
+
|
|
137
|
+
const lane = feature.lane;
|
|
138
|
+
const escalationCount = feature.escalationCount ?? 0;
|
|
139
|
+
const decision = escalate({ gate: 'test', passed: false }, lane, escalationCount, currentPhase);
|
|
140
|
+
|
|
141
|
+
if (decision === 'STOP') {
|
|
142
|
+
writeCheckpoint(featureDir, { featureCode, lane, decision: 'STOP', reEntryPhase: null, escalationCount });
|
|
143
|
+
return { action: 'stop', lane, escalationCount };
|
|
144
|
+
}
|
|
145
|
+
if (!decision) return { action: 'none' };
|
|
146
|
+
|
|
147
|
+
// Widen the profile to match the ESCALATED lane (not always full) so lane and
|
|
148
|
+
// profile stay consistent and each rung actually adds phases.
|
|
149
|
+
const widenedProfile = floorProfileToLane(feature.profile ?? {}, decision.nextLane);
|
|
150
|
+
validateFeatureFields({ lane: decision.nextLane, estimateSource: 'escalated' });
|
|
151
|
+
// Write the checkpoint BEFORE stamping triageTimestamp so the checkpoint file's
|
|
152
|
+
// mtime is older than the stamp — otherwise isTriageStale would treat the just-
|
|
153
|
+
// escalated feature as stale on the next build and overwrite the escalation.
|
|
154
|
+
writeCheckpoint(featureDir, {
|
|
155
|
+
featureCode,
|
|
156
|
+
lane: decision.nextLane,
|
|
157
|
+
decision: 'escalate',
|
|
158
|
+
reEntryPhase: decision.reEntryPhase,
|
|
159
|
+
escalationCount: escalationCount + 1,
|
|
160
|
+
});
|
|
161
|
+
await provider.putFeature(featureCode, {
|
|
162
|
+
...feature,
|
|
163
|
+
lane: decision.nextLane,
|
|
164
|
+
estimateSource: 'escalated',
|
|
165
|
+
escalationCount: escalationCount + 1,
|
|
166
|
+
profile: widenedProfile,
|
|
167
|
+
triageTimestamp: new Date().toISOString(),
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
action: 'escalate',
|
|
171
|
+
from: lane,
|
|
172
|
+
to: decision.nextLane,
|
|
173
|
+
reEntryPhase: decision.reEntryPhase,
|
|
174
|
+
escalationCount: escalationCount + 1,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function writeCheckpoint(featureDir, { featureCode, lane, decision, reEntryPhase, escalationCount }) {
|
|
179
|
+
if (!featureDir) return;
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(featureDir, { recursive: true });
|
|
182
|
+
const lines = [
|
|
183
|
+
`# COMP-TRIAGE-5 Escalation Checkpoint — ${featureCode}`,
|
|
184
|
+
'',
|
|
185
|
+
`- Decision: ${decision}`,
|
|
186
|
+
`- Lane: ${lane}`,
|
|
187
|
+
reEntryPhase ? `- Re-enter at phase: ${reEntryPhase}` : '- Re-enter: n/a (STOP — human handoff)',
|
|
188
|
+
`- Escalation count: ${escalationCount}`,
|
|
189
|
+
'- Trigger: ship-time test gate failed (E3 Expand)',
|
|
190
|
+
'',
|
|
191
|
+
decision === 'STOP'
|
|
192
|
+
? 'Escalation bound reached. A human should investigate before re-running.'
|
|
193
|
+
: `Re-run \`compose build ${featureCode}\` — it reads the escalated lane and runs the heavier phases.`,
|
|
194
|
+
'',
|
|
195
|
+
];
|
|
196
|
+
writeFileSync(join(featureDir, 'escalation-checkpoint.md'), lines.join('\n'), 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
/* best-effort — a checkpoint failure must never break the build */
|
|
199
|
+
}
|
|
200
|
+
}
|
package/lib/triage.js
CHANGED
|
@@ -174,6 +174,150 @@ function deriveProfile(signals) {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Front-seam scope estimation (doc-free)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
// Lane order, smaller index = smaller scope / more conservative.
|
|
182
|
+
const LANE_ORDER = { trivial: 0, standard: 1, complex: 2 };
|
|
183
|
+
|
|
184
|
+
// Verbs that denote a single, well-defined action. Presence (+ an explicit
|
|
185
|
+
// file path) is what earns 'high' confidence; absence (+ no path) is what
|
|
186
|
+
// earns 'low' confidence and triggers the safety clamp.
|
|
187
|
+
const UNAMBIGUOUS_VERB_RE =
|
|
188
|
+
/\b(fix(?:e[ds]|ing)?|renam(?:e[ds]?|ing)|add(?:ed|s|ing)?|remov(?:e[ds]?|ing)|delet(?:e[ds]?|ing)|updat(?:e[ds]?|ing)|creat(?:e[ds]?|ing)|implement(?:ed|s|ing)?|refactor(?:ed|s|ing)?|bump(?:ed|s|ing)?|patch(?:ed|es|ing)?|mov(?:e[ds]?|ing)|extract(?:ed|s|ing)?|replac(?:e[ds]?|ing)|revert(?:ed|s|ing)?|rewrit(?:e|es|ing)|rewrote)\b/i;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Map a triage tier (0-4) to a coarse lane.
|
|
192
|
+
*
|
|
193
|
+
* @param {number} tier
|
|
194
|
+
* @returns {'trivial'|'standard'|'complex'}
|
|
195
|
+
*/
|
|
196
|
+
export function tierToLane(tier) {
|
|
197
|
+
if (tier <= 1) return 'trivial';
|
|
198
|
+
if (tier === 2) return 'standard';
|
|
199
|
+
return 'complex';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clamp a lane up to at least `minimum` (never narrows).
|
|
204
|
+
*
|
|
205
|
+
* @param {string} lane
|
|
206
|
+
* @param {string} minimum
|
|
207
|
+
* @returns {string}
|
|
208
|
+
*/
|
|
209
|
+
function clampLaneMin(lane, minimum) {
|
|
210
|
+
return LANE_ORDER[lane] < LANE_ORDER[minimum] ? minimum : lane;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Estimate scope from request text alone — the DOC-FREE front seam.
|
|
215
|
+
*
|
|
216
|
+
* Unlike `runTriage`, this never reads plan.md/blueprint.md/design.md (or
|
|
217
|
+
* any file at all). It derives the same `signals` shape `deriveProfile`
|
|
218
|
+
* expects purely from the request text and caller-supplied repo-signal
|
|
219
|
+
* hints, so it can run before any design doc exists — before the genesis
|
|
220
|
+
* phase, at feature-creation time.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} request - Free-text feature/bug request description.
|
|
223
|
+
* @param {{ files?: string[] }} [repoSignals] - Candidate file paths the
|
|
224
|
+
* request plausibly touches (caller-supplied, e.g. from a git-diff or
|
|
225
|
+
* grep hint). Minimal shape: `{ files: string[] }`.
|
|
226
|
+
* @returns {{ tier: number, profile: object, lane: 'trivial'|'standard'|'complex', confidence: 'high'|'medium'|'low', rationale: string }}
|
|
227
|
+
*/
|
|
228
|
+
// Reconcile the tier-derived profile with the (possibly clamped) lane so the two
|
|
229
|
+
// never disagree. The safety clamp raises the LANE, but skip_if is driven by the
|
|
230
|
+
// PROFILE — without this floor a clamped-to-standard lane would still skip
|
|
231
|
+
// verification, defeating the clamp. Standard keeps verification; complex keeps
|
|
232
|
+
// every phase; trivial is left as deriveProfile set it.
|
|
233
|
+
export function floorProfileToLane(profile, lane) {
|
|
234
|
+
if (lane === 'complex') {
|
|
235
|
+
return { needs_prd: true, needs_architecture: true, needs_verification: true, needs_report: true };
|
|
236
|
+
}
|
|
237
|
+
if (lane === 'standard') {
|
|
238
|
+
return { ...profile, needs_verification: true };
|
|
239
|
+
}
|
|
240
|
+
return { ...profile };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function estimateScope(request, repoSignals = {}) {
|
|
244
|
+
const text = request ?? '';
|
|
245
|
+
const hintedFiles = Array.isArray(repoSignals?.files) ? repoSignals.files : [];
|
|
246
|
+
|
|
247
|
+
const textPaths = extractFilePaths(text);
|
|
248
|
+
const uniquePaths = new Set([...textPaths, ...hintedFiles]);
|
|
249
|
+
const pathList = [...uniquePaths];
|
|
250
|
+
|
|
251
|
+
const taskCount = countTasks(text);
|
|
252
|
+
const securityPaths = anyMatch(pathList, SECURITY_PATTERNS);
|
|
253
|
+
const corePaths = anyMatch(pathList, CORE_PATTERNS);
|
|
254
|
+
|
|
255
|
+
const signals = {
|
|
256
|
+
fileCount: uniquePaths.size,
|
|
257
|
+
securityPaths,
|
|
258
|
+
corePaths,
|
|
259
|
+
taskCount,
|
|
260
|
+
hasDesignDoc: false,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const { tier, profile, rationale } = deriveProfile(signals);
|
|
264
|
+
|
|
265
|
+
let lane = tierToLane(tier);
|
|
266
|
+
// Belt-and-suspenders: a security/core path hit must never resolve below
|
|
267
|
+
// 'standard', independent of how deriveProfile's own tier logic evolves.
|
|
268
|
+
if (securityPaths || corePaths) {
|
|
269
|
+
lane = clampLaneMin(lane, 'standard');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const hasPaths = uniquePaths.size > 0;
|
|
273
|
+
const hasUnambiguousVerb = UNAMBIGUOUS_VERB_RE.test(text);
|
|
274
|
+
let confidence;
|
|
275
|
+
if (hasPaths && hasUnambiguousVerb) {
|
|
276
|
+
confidence = 'high';
|
|
277
|
+
} else if (!hasPaths && !hasUnambiguousVerb) {
|
|
278
|
+
confidence = 'low';
|
|
279
|
+
} else {
|
|
280
|
+
confidence = 'medium';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Safety clamp: under-scoping is the only dangerous error. Low confidence
|
|
284
|
+
// (no named paths, no clear verb) can never resolve to 'trivial'.
|
|
285
|
+
if (confidence === 'low') {
|
|
286
|
+
lane = clampLaneMin(lane, 'standard');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { tier, profile: floorProfileToLane(profile, lane), lane, confidence, rationale };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Map a triage tier to a persisted complexity label.
|
|
294
|
+
*
|
|
295
|
+
* @param {number} tier - 0-4
|
|
296
|
+
* @returns {'S'|'M'|'L'|'XL'}
|
|
297
|
+
*/
|
|
298
|
+
export function tierToComplexity(tier) {
|
|
299
|
+
if (tier <= 1) return 'S';
|
|
300
|
+
if (tier === 2) return 'M';
|
|
301
|
+
if (tier === 3) return 'L';
|
|
302
|
+
return 'XL';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Return the more conservative (smaller-scope) of two lanes.
|
|
307
|
+
* Order: trivial < standard < complex.
|
|
308
|
+
*
|
|
309
|
+
* Used by refinement/escalation call sites to enforce "narrow-only" — a
|
|
310
|
+
* later pass may only shrink scope, never widen it (widening is
|
|
311
|
+
* escalation's job, via `lib/escalation.js`, not refinement's).
|
|
312
|
+
*
|
|
313
|
+
* @param {'trivial'|'standard'|'complex'} a
|
|
314
|
+
* @param {'trivial'|'standard'|'complex'} b
|
|
315
|
+
* @returns {'trivial'|'standard'|'complex'}
|
|
316
|
+
*/
|
|
317
|
+
export function narrowerLane(a, b) {
|
|
318
|
+
return LANE_ORDER[a] <= LANE_ORDER[b] ? a : b;
|
|
319
|
+
}
|
|
320
|
+
|
|
177
321
|
// ---------------------------------------------------------------------------
|
|
178
322
|
// Public API
|
|
179
323
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.58-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,9 +27,16 @@ export function attachSettingsRoutes(app, { settingsStore, broadcastMessage }) {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
app.post('/api/settings/reset', (req, res) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
try {
|
|
31
|
+
const section = req.body?.section || undefined;
|
|
32
|
+
const updated = settingsStore.reset(section);
|
|
33
|
+
broadcastMessage({ type: 'settingsUpdated', settings: updated });
|
|
34
|
+
res.json(updated);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// AUDIT-2: reset() can now throw if the write fails; surface it as JSON
|
|
37
|
+
// {error} (mirrors PATCH) so the client sees a failure instead of an
|
|
38
|
+
// unhandled 500 that apiCall's res.json() cannot parse.
|
|
39
|
+
res.status(500).json({ error: err.message });
|
|
40
|
+
}
|
|
34
41
|
});
|
|
35
42
|
}
|
package/server/settings-store.js
CHANGED
|
@@ -35,11 +35,23 @@ export class SettingsStore {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
_save() {
|
|
38
|
+
// AUDIT-2: atomic write (temp + rename) so a partial/failed write can never
|
|
39
|
+
// truncate the live settings.json — the real file is only ever replaced by an
|
|
40
|
+
// atomic rename of a fully-written temp file. On failure the live file is left
|
|
41
|
+
// untouched (last-good), which keeps disk consistent with the in-memory
|
|
42
|
+
// rollback performed by update()/reset().
|
|
43
|
+
const tmp = `${this._file}.tmp-${process.pid}`;
|
|
38
44
|
try {
|
|
39
45
|
fs.mkdirSync(path.dirname(this._file), { recursive: true });
|
|
40
|
-
fs.writeFileSync(
|
|
46
|
+
fs.writeFileSync(tmp, JSON.stringify(this._userSettings, null, 2), 'utf-8');
|
|
47
|
+
fs.renameSync(tmp, this._file);
|
|
41
48
|
} catch (err) {
|
|
49
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* ignore cleanup failure */ }
|
|
42
50
|
console.error('[settings] Failed to save settings:', err.message);
|
|
51
|
+
// Never report a save as successful when it did not reach disk. update()/
|
|
52
|
+
// reset() roll back in-memory state and rethrow; the routes surface it as an
|
|
53
|
+
// error response so the modal shows "Save failed" instead of "Saved".
|
|
54
|
+
throw new Error(`Failed to persist settings: ${err.message}`);
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -63,6 +75,7 @@ export class SettingsStore {
|
|
|
63
75
|
gate_threshold: null, // null = off, number = min_score required
|
|
64
76
|
weights: {}, // dimension weight overrides (must sum to 1.0 ± 0.01)
|
|
65
77
|
},
|
|
78
|
+
governance: { phases: {} },
|
|
66
79
|
// Claude thinking/effort controls. mode='tier' inherits the tier default
|
|
67
80
|
// (critical → adaptive+xhigh, standard → adaptive+high, fast → off).
|
|
68
81
|
// effort=null likewise inherits the tier default.
|
|
@@ -93,37 +106,61 @@ export class SettingsStore {
|
|
|
93
106
|
weights: { ...defaults.health.weights, ...user.health?.weights },
|
|
94
107
|
},
|
|
95
108
|
thinking: { ...defaults.thinking, ...user.thinking },
|
|
109
|
+
governance: {
|
|
110
|
+
...defaults.governance,
|
|
111
|
+
...user.governance,
|
|
112
|
+
phases: { ...defaults.governance.phases, ...user.governance?.phases },
|
|
113
|
+
},
|
|
96
114
|
};
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
/** Validate and apply a partial settings update. */
|
|
100
118
|
update(patch) {
|
|
101
119
|
this._validate(patch);
|
|
120
|
+
// AUDIT-2: transactional apply — if the persist below fails, roll memory
|
|
121
|
+
// back so the store never reports a failed save while holding the change.
|
|
122
|
+
const prev = JSON.parse(JSON.stringify(this._userSettings));
|
|
102
123
|
// Deep merge into user settings
|
|
103
|
-
for (const section of ['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking']) {
|
|
124
|
+
for (const section of ['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking', 'governance']) {
|
|
104
125
|
if (patch[section]) {
|
|
105
126
|
if (!this._userSettings[section]) this._userSettings[section] = {};
|
|
106
127
|
if (section === 'iterations') {
|
|
107
128
|
for (const [key, val] of Object.entries(patch.iterations)) {
|
|
108
129
|
this._userSettings.iterations[key] = { ...this._userSettings.iterations[key], ...val };
|
|
109
130
|
}
|
|
131
|
+
} else if (section === 'governance') {
|
|
132
|
+
this._userSettings.governance.phases = {
|
|
133
|
+
...this._userSettings.governance.phases,
|
|
134
|
+
...patch.governance.phases,
|
|
135
|
+
};
|
|
110
136
|
} else {
|
|
111
137
|
Object.assign(this._userSettings[section], patch[section]);
|
|
112
138
|
}
|
|
113
139
|
}
|
|
114
140
|
}
|
|
115
|
-
|
|
141
|
+
try {
|
|
142
|
+
this._save();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
this._userSettings = prev;
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
116
147
|
return this.get();
|
|
117
148
|
}
|
|
118
149
|
|
|
119
150
|
/** Reset user overrides. If section given, reset only that section. */
|
|
120
151
|
reset(section) {
|
|
152
|
+
const prev = JSON.parse(JSON.stringify(this._userSettings));
|
|
121
153
|
if (section) {
|
|
122
154
|
delete this._userSettings[section];
|
|
123
155
|
} else {
|
|
124
156
|
this._userSettings = {};
|
|
125
157
|
}
|
|
126
|
-
|
|
158
|
+
try {
|
|
159
|
+
this._save();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
this._userSettings = prev;
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
127
164
|
return this.get();
|
|
128
165
|
}
|
|
129
166
|
|
|
@@ -132,7 +169,7 @@ export class SettingsStore {
|
|
|
132
169
|
|
|
133
170
|
// Reject unknown top-level keys
|
|
134
171
|
for (const key of Object.keys(patch)) {
|
|
135
|
-
if (!['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking'].includes(key)) {
|
|
172
|
+
if (!['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking', 'governance'].includes(key)) {
|
|
136
173
|
throw new Error(`Unknown settings section: ${key}`);
|
|
137
174
|
}
|
|
138
175
|
}
|
|
@@ -145,6 +182,17 @@ export class SettingsStore {
|
|
|
145
182
|
}
|
|
146
183
|
}
|
|
147
184
|
|
|
185
|
+
if (patch.governance) {
|
|
186
|
+
if (!patch.governance.phases || typeof patch.governance.phases !== 'object' || Array.isArray(patch.governance.phases)) {
|
|
187
|
+
throw new Error('Invalid governance.phases: must be an object');
|
|
188
|
+
}
|
|
189
|
+
for (const [phase, mode] of Object.entries(patch.governance.phases)) {
|
|
190
|
+
if (mode !== null && !validModes.has(mode)) {
|
|
191
|
+
throw new Error(`Invalid governance policy mode for ${phase}: ${mode}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
148
196
|
if (patch.iterations) {
|
|
149
197
|
for (const [type, config] of Object.entries(patch.iterations)) {
|
|
150
198
|
if (config.maxIterations !== undefined) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ag as o,ah as n}from"./App-B4vuNX1g.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-CKNWOjmw.js";import{_ as i}from"./App-B4vuNX1g.js";import"./chunk-5VM5RSS4-BkOdBdcj.js";import"./chunk-XXDRQBXY-Ct9K5k9G.js";import"./chunk-VR4S4FIN-Bx3WjdJ3.js";import"./chunk-32BRIVSS-CLJH-LjM.js";import"./mobile-CNLMhdFP.js";import"./index-D4TlupB7.js";import"./graph-xkel59g2.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-CKNWOjmw.js";import{_ as i}from"./App-B4vuNX1g.js";import"./chunk-5VM5RSS4-BkOdBdcj.js";import"./chunk-XXDRQBXY-Ct9K5k9G.js";import"./chunk-VR4S4FIN-Bx3WjdJ3.js";import"./chunk-32BRIVSS-CLJH-LjM.js";import"./mobile-CNLMhdFP.js";import"./index-D4TlupB7.js";import"./graph-xkel59g2.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|