@davidorex/pi-context 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +27 -11
- package/dist/block-api.d.ts +13 -0
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +28 -3
- package/dist/block-api.js.map +1 -1
- package/dist/content-hash.d.ts +13 -0
- package/dist/content-hash.d.ts.map +1 -1
- package/dist/content-hash.js +16 -0
- package/dist/content-hash.js.map +1 -1
- package/dist/context-dir.d.ts +12 -0
- package/dist/context-dir.d.ts.map +1 -1
- package/dist/context-dir.js +14 -0
- package/dist/context-dir.js.map +1 -1
- package/dist/context-sdk.d.ts +71 -33
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +547 -149
- package/dist/context-sdk.js.map +1 -1
- package/dist/context.d.ts +213 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +119 -5
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +595 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2225 -55
- package/dist/index.js.map +1 -1
- package/dist/lens-view.d.ts +0 -5
- package/dist/lens-view.d.ts.map +1 -1
- package/dist/lens-view.js +43 -1
- package/dist/lens-view.js.map +1 -1
- package/dist/migration-registry-loader.d.ts +36 -12
- package/dist/migration-registry-loader.d.ts.map +1 -1
- package/dist/migration-registry-loader.js +79 -17
- package/dist/migration-registry-loader.js.map +1 -1
- package/dist/migrations-store.d.ts +45 -18
- package/dist/migrations-store.d.ts.map +1 -1
- package/dist/migrations-store.js +56 -22
- package/dist/migrations-store.js.map +1 -1
- package/dist/ops-registry.d.ts +16 -0
- package/dist/ops-registry.d.ts.map +1 -1
- package/dist/ops-registry.js +352 -117
- package/dist/ops-registry.js.map +1 -1
- package/dist/pending-blocked-store.d.ts +83 -0
- package/dist/pending-blocked-store.d.ts.map +1 -0
- package/dist/pending-blocked-store.js +93 -0
- package/dist/pending-blocked-store.js.map +1 -0
- package/dist/promote-item.d.ts.map +1 -1
- package/dist/promote-item.js +41 -12
- package/dist/promote-item.js.map +1 -1
- package/dist/roadmap-plan.d.ts +121 -99
- package/dist/roadmap-plan.d.ts.map +1 -1
- package/dist/roadmap-plan.js +281 -345
- package/dist/roadmap-plan.js.map +1 -1
- package/dist/schema-merge.d.ts +26 -0
- package/dist/schema-merge.d.ts.map +1 -0
- package/dist/schema-merge.js +176 -0
- package/dist/schema-merge.js.map +1 -0
- package/dist/status-vocab.d.ts +12 -2
- package/dist/status-vocab.d.ts.map +1 -1
- package/dist/status-vocab.js +14 -1
- package/dist/status-vocab.js.map +1 -1
- package/package.json +2 -1
- package/samples/blocks/milestone.json +3 -0
- package/samples/blocks/session-notes.json +1 -0
- package/samples/conception.json +358 -15
- package/samples/migrations.json +8 -0
- package/samples/schemas/context-contracts.schema.json +4 -0
- package/samples/schemas/conventions.schema.json +4 -0
- package/samples/schemas/decisions.schema.json +4 -0
- package/samples/schemas/features.schema.json +4 -0
- package/samples/schemas/framework-gaps.schema.json +5 -1
- package/samples/schemas/issues.schema.json +30 -2
- package/samples/schemas/layer-plans.schema.json +6 -2
- package/samples/schemas/milestone.schema.json +79 -0
- package/samples/schemas/phase.schema.json +4 -0
- package/samples/schemas/rationale.schema.json +4 -0
- package/samples/schemas/requirements.schema.json +4 -0
- package/samples/schemas/research.schema.json +5 -1
- package/samples/schemas/session-notes.schema.json +89 -0
- package/samples/schemas/spec-reviews.schema.json +4 -0
- package/samples/schemas/story.schema.json +8 -0
- package/samples/schemas/tasks.schema.json +4 -0
- package/samples/schemas/verification.schema.json +4 -0
- package/samples/schemas/work-orders.schema.json +6 -2
- package/schemas/config.schema.json +101 -3
- package/schemas/migrations.schema.json +25 -0
- package/schemas/pending-blocked.schema.json +190 -0
- package/skill-narrative.md +14 -10
- package/skills/pi-context/SKILL.md +127 -49
- package/skills/pi-context/references/bundled-resources.md +7 -2
package/dist/roadmap-plan.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Roadmap
|
|
2
|
+
* Roadmap substrate primitives — the roadmap as a DERIVED view over the
|
|
3
|
+
* milestone block + authored relations (FGAP-042). There is no roadmap block,
|
|
4
|
+
* no roadmap.json, and no ROADMAP- id: the roadmap IS the milestone-block
|
|
5
|
+
* items ordered by the `milestone_precedes_milestone` closure-table edges,
|
|
6
|
+
* with per-milestone membership resolved through `phase_positioned_in_milestone`
|
|
7
|
+
* and per-phase membership through `task_positioned_in_phase`.
|
|
3
8
|
*
|
|
4
|
-
* Module landing pattern follows lens-view.ts: pure functions take cwd
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* Module landing pattern follows lens-view.ts: pure functions take cwd,
|
|
10
|
+
* return structured results or { error } records. Subcommand handlers and
|
|
11
|
+
* tool execute() shells in index.ts are thin wrappers that route results to
|
|
12
|
+
* ctx.ui.notify, pi.sendMessage, or AgentToolResult.
|
|
8
13
|
*
|
|
9
14
|
* Status vocabulary normalization: STATUS_VOCABULARY_DEFAULTS maps each
|
|
10
15
|
* schema's native status enum (issues uses open|resolved|deferred;
|
|
@@ -16,23 +21,17 @@
|
|
|
16
21
|
* `config.status_buckets` over the defaults — users override per-project
|
|
17
22
|
* by editing config.json (FGAP-013 closed).
|
|
18
23
|
*
|
|
19
|
-
* Diagnostic display strings:
|
|
24
|
+
* Diagnostic display strings: validateRoadmap() emits issues whose
|
|
20
25
|
* `code` field is an opaque slug; the human-readable message is
|
|
21
26
|
* resolved via `diagMessage(cwd, code, fallback)` which checks
|
|
22
27
|
* `config.display_strings[code]` and falls back to embedded English.
|
|
23
28
|
* This is the pi-context divergence: main hardcodes English; pi-context
|
|
24
29
|
* routes the message through config so per-project / per-locale
|
|
25
30
|
* overrides are config-driven.
|
|
26
|
-
*
|
|
27
|
-
* Topological sort + cycle detection (topoSort) is shared utility used by
|
|
28
|
-
* loadRoadmap (over phase_depends_on edges) and loadPlan (over
|
|
29
|
-
* plan_item_depends_on edges) per DEC-0012's edges-only authoring contract.
|
|
30
31
|
*/
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { availableBlocks } from "./context-sdk.js";
|
|
32
|
+
import { counterEndpoint, endpointKey, loadContext, primaryEndpoint, } from "./context.js";
|
|
33
|
+
import { buildIdIndex, currentState } from "./context-sdk.js";
|
|
34
34
|
import { registerLensValidator } from "./lens-validator.js";
|
|
35
|
-
import { loadLensView } from "./lens-view.js";
|
|
36
35
|
import { resolveStatusVocabulary, STATUS_VOCABULARY_DEFAULTS } from "./status-vocab.js";
|
|
37
36
|
import { topoSort } from "./topo.js";
|
|
38
37
|
export { resolveStatusVocabulary, STATUS_VOCABULARY_DEFAULTS };
|
|
@@ -109,8 +108,8 @@ function diagMessage(cwd, code, fallback) {
|
|
|
109
108
|
* cycles — array of cycle paths, each as a string[] starting and
|
|
110
109
|
* ending with the same id. Empty when graph is acyclic.
|
|
111
110
|
*
|
|
112
|
-
* Caller chooses whether cycles are fatal:
|
|
113
|
-
*
|
|
111
|
+
* Caller chooses whether cycles are fatal: validateRoadmap surfaces
|
|
112
|
+
* roadmap_milestone_cycle; loadRoadmap returns the cycles[] alongside a
|
|
114
113
|
* partial order so renderRoadmap can show what it could and what
|
|
115
114
|
* couldn't be ordered.
|
|
116
115
|
*/
|
|
@@ -118,326 +117,293 @@ function diagMessage(cwd, code, fallback) {
|
|
|
118
117
|
// consume it without a roadmap-plan <-> context-sdk import cycle. Re-exported
|
|
119
118
|
// here to preserve the existing import surface (barrel + roadmap-plan.test).
|
|
120
119
|
export { topoSort };
|
|
121
|
-
|
|
120
|
+
// ── Derived roadmap over milestone_precedes_milestone (FGAP-042) ────────────
|
|
121
|
+
//
|
|
122
|
+
// Roadmap order is the precedes-DAG over milestone-block items: authoring the
|
|
123
|
+
// order = appending milestone_precedes_milestone edges to relations.json. The
|
|
124
|
+
// renderer is pure-textual: per-milestone adjacency lines are sourced from
|
|
125
|
+
// MilestoneRoadmapView.edges, not inferred from `order` consecutive pairs (a
|
|
126
|
+
// fabrication pattern in branching DAGs).
|
|
127
|
+
// The roadmap selects its three domain relations by canonical_id
|
|
128
|
+
// (milestone_precedes_milestone for the precedes DAG, phase_positioned_in_milestone
|
|
129
|
+
// + task_positioned_in_phase for the membership tiers), but the ORIENTATION of
|
|
130
|
+
// each edge — which endpoint is the predecessor / container — is read from the
|
|
131
|
+
// relation's declared `role_direction` (FGAP-113) via primaryEndpoint /
|
|
132
|
+
// counterEndpoint, not hardcoded parent/child picks. The former module constants
|
|
133
|
+
// are inlined at their (few) use sites so no relation-name literal is duplicated
|
|
134
|
+
// as an orientation carrier.
|
|
122
135
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
136
|
+
* The declared orientation of a roadmap relation, or the roadmap's `fallback`
|
|
137
|
+
* when the config does not register a `role_direction` for it (FGAP-113). The
|
|
138
|
+
* fallbacks preserve the pre-metadata hardcoded orientation exactly:
|
|
139
|
+
* `milestone_precedes_milestone` → `as_parent` (predecessor at `edge.parent`);
|
|
140
|
+
* the two `*_positioned_in_*` membership relations → `as_child` (container at
|
|
141
|
+
* `edge.child`, member at `edge.parent`).
|
|
126
142
|
*/
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
const data = readBlock(cwd, "roadmap");
|
|
130
|
-
return Array.isArray(data.roadmaps) ? data.roadmaps : [];
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
143
|
+
function roadmapRoleDirection(config, relationType, fallback) {
|
|
144
|
+
return config?.relation_types?.find((r) => r.canonical_id === relationType)?.role_direction ?? fallback;
|
|
135
145
|
}
|
|
146
|
+
// Error codes flip status to invalid; warning codes to warnings; info codes
|
|
147
|
+
// (roadmap_milestone_isolated) NEVER affect status and are excluded from the
|
|
148
|
+
// lens-validator merge.
|
|
149
|
+
const ROADMAP_ERROR_CODES = new Set([
|
|
150
|
+
"roadmap_precedes_endpoint_missing",
|
|
151
|
+
"roadmap_milestone_cycle",
|
|
152
|
+
"roadmap_milestone_missing",
|
|
153
|
+
]);
|
|
154
|
+
const ROADMAP_WARNING_CODES = new Set(["roadmap_status_unknown_value"]);
|
|
136
155
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* separate diagnostic where appropriate).
|
|
142
|
-
*/
|
|
143
|
-
function evaluateMilestone(cwd, m) {
|
|
144
|
-
if (!m.evidence_block || !m.evidence_query)
|
|
145
|
-
return false;
|
|
146
|
-
let items;
|
|
147
|
-
try {
|
|
148
|
-
const data = readBlock(cwd, m.evidence_block);
|
|
149
|
-
const arrayKey = Object.keys(data).find((k) => Array.isArray(data[k]));
|
|
150
|
-
if (!arrayKey)
|
|
151
|
-
return false;
|
|
152
|
-
items = data[arrayKey];
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
const query = m.evidence_query;
|
|
158
|
-
for (const item of items) {
|
|
159
|
-
let match = true;
|
|
160
|
-
for (const [k, v] of Object.entries(query)) {
|
|
161
|
-
if (item[k] !== v) {
|
|
162
|
-
match = false;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (match)
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Load a single roadmap by id. Returns the materialized RoadmapView or a
|
|
173
|
-
* structured { error } when config / block / id is missing.
|
|
156
|
+
* Load the derived roadmap. Returns the materialized MilestoneRoadmapView or
|
|
157
|
+
* a structured { error } when no config exists. Zero milestone-block items is
|
|
158
|
+
* a VALID empty view (not an error) — the roadmap is opt-in by authoring
|
|
159
|
+
* milestones + edges.
|
|
174
160
|
*
|
|
175
161
|
* Algorithm:
|
|
176
|
-
* 1. Load context; bail when no
|
|
177
|
-
* 2.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
162
|
+
* 1. Load context; bail when no <substrate-dir>/config.json.
|
|
163
|
+
* 2. Index the substrate; milestones = index items whose block is
|
|
164
|
+
* "milestone" (stable block scan order).
|
|
165
|
+
* 3. Scope the milestone_precedes_milestone edges to those whose BOTH
|
|
166
|
+
* endpoints are milestone ids; topoSort (preds = parents of a
|
|
167
|
+
* milestone's incoming precedes edges) derives order + cycles.
|
|
168
|
+
* 4. Per milestone: status + phaseCount from currentState(cwd).milestones
|
|
169
|
+
* (the config-declared derived rollup; a milestone absent from that list
|
|
170
|
+
* reports the rollup pattern's incomplete default "planned" with
|
|
171
|
+
* phaseCount 0); phases = parents of phase_positioned_in_milestone edges
|
|
172
|
+
* (child = the milestone), each with tasks = parents of
|
|
173
|
+
* task_positioned_in_phase edges (child = the phase) and a
|
|
174
|
+
* rollupPhaseStatus over the resolved task items; the milestone-level
|
|
175
|
+
* rollup aggregates all member phases' task items in one call.
|
|
185
176
|
*
|
|
186
177
|
* The returned `edges` field is the authoritative scoped subset that the
|
|
187
|
-
* renderer consumes for per-
|
|
178
|
+
* renderer consumes for per-milestone adjacency lines.
|
|
188
179
|
*/
|
|
189
|
-
export function loadRoadmap(cwd
|
|
180
|
+
export function loadRoadmap(cwd) {
|
|
190
181
|
const ctx = loadContext(cwd);
|
|
191
182
|
if (!ctx.config) {
|
|
192
183
|
return {
|
|
193
184
|
error: "No <substrate-dir>/config.json — run /context init <substrate-dir> first, then declare lenses + install assets.",
|
|
194
185
|
};
|
|
195
186
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
const roadmap = roadmaps.find((r) => r.id === roadmapId);
|
|
203
|
-
if (!roadmap) {
|
|
204
|
-
const known = roadmaps.map((r) => r.id).join(", ") || "(none)";
|
|
205
|
-
return { error: `Roadmap '${roadmapId}' not found. Known: ${known}` };
|
|
187
|
+
const index = buildIdIndex(cwd);
|
|
188
|
+
const milestoneItems = index.items.filter((loc) => loc.block === "milestone");
|
|
189
|
+
if (milestoneItems.length === 0) {
|
|
190
|
+
return { milestones: [], order: [], cycles: [], edges: [] };
|
|
206
191
|
}
|
|
192
|
+
const milestoneIds = new Set(milestoneItems.map((loc) => loc.id));
|
|
193
|
+
// Precedes DAG: predecessor at the PRIMARY endpoint (as_parent fallback), so a
|
|
194
|
+
// milestone's preds are the PRIMARY endpoints of edges where the milestone is
|
|
195
|
+
// the COUNTER (successor) endpoint. The endpoint-membership filter below is
|
|
196
|
+
// orientation-independent (both endpoints must be milestones either way).
|
|
197
|
+
const precedesRel = "milestone_precedes_milestone";
|
|
198
|
+
const precedesDir = roadmapRoleDirection(ctx.config, precedesRel, "as_parent");
|
|
199
|
+
const edges = ctx.relations.filter((e) => e.relation_type === precedesRel &&
|
|
200
|
+
milestoneIds.has(endpointKey(e.parent)) &&
|
|
201
|
+
milestoneIds.has(endpointKey(e.child)));
|
|
202
|
+
const { order, cycles } = topoSort(milestoneItems, (loc) => loc.id, (loc) => edges
|
|
203
|
+
.filter((e) => endpointKey(counterEndpoint(e, precedesDir)) === loc.id)
|
|
204
|
+
.map((e) => endpointKey(primaryEndpoint(e, precedesDir))));
|
|
205
|
+
const phaseMembershipRel = "phase_positioned_in_milestone";
|
|
206
|
+
const phaseDir = roadmapRoleDirection(ctx.config, phaseMembershipRel, "as_child");
|
|
207
|
+
const taskMembershipRel = "task_positioned_in_phase";
|
|
208
|
+
const taskDir = roadmapRoleDirection(ctx.config, taskMembershipRel, "as_child");
|
|
207
209
|
const vocabulary = resolveStatusVocabulary(cwd);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
// One currentState call: the config-declared milestone rollup supplies each
|
|
211
|
+
// milestone's derived status (reached/planned) + phaseCount.
|
|
212
|
+
const derivedMilestones = new Map(currentState(cwd).milestones.map((m) => [m.id, m]));
|
|
213
|
+
const milestones = milestoneItems.map((loc) => {
|
|
214
|
+
const derived = derivedMilestones.get(loc.id);
|
|
215
|
+
// Member phases: the milestone is the CONTAINER (primary); its members are
|
|
216
|
+
// the COUNTER endpoints of phase_positioned_in_milestone edges pointing at it.
|
|
217
|
+
const memberPhaseIds = ctx.relations
|
|
218
|
+
.filter((e) => e.relation_type === phaseMembershipRel && endpointKey(primaryEndpoint(e, phaseDir)) === loc.id)
|
|
219
|
+
.map((e) => endpointKey(counterEndpoint(e, phaseDir)));
|
|
220
|
+
const allTaskItems = [];
|
|
221
|
+
const phases = memberPhaseIds.map((phaseId) => {
|
|
222
|
+
const phaseLoc = index.byRefname.get(phaseId);
|
|
223
|
+
const taskIds = ctx.relations
|
|
224
|
+
.filter((e) => e.relation_type === taskMembershipRel && endpointKey(primaryEndpoint(e, taskDir)) === phaseId)
|
|
225
|
+
.map((e) => endpointKey(counterEndpoint(e, taskDir)));
|
|
226
|
+
const taskItems = taskIds.map((taskId) => {
|
|
227
|
+
const taskLoc = index.byRefname.get(taskId);
|
|
228
|
+
return taskLoc ? { ...taskLoc.item, id: taskId } : { id: taskId };
|
|
229
|
+
});
|
|
230
|
+
allTaskItems.push(...taskItems);
|
|
231
|
+
const tasks = taskItems.map((item) => ({
|
|
232
|
+
id: item.id,
|
|
233
|
+
...(typeof item.status === "string" ? { status: item.status } : {}),
|
|
234
|
+
...(typeof item.title === "string" ? { title: item.title } : {}),
|
|
235
|
+
}));
|
|
236
|
+
return {
|
|
237
|
+
id: phaseId,
|
|
238
|
+
...(typeof phaseLoc?.item.name === "string" ? { name: phaseLoc.item.name } : {}),
|
|
239
|
+
...(typeof phaseLoc?.item.status === "string" ? { status: phaseLoc.item.status } : {}),
|
|
240
|
+
tasks,
|
|
241
|
+
rollup: rollupPhaseStatus(taskItems, vocabulary),
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
id: loc.id,
|
|
246
|
+
...(typeof loc.item.name === "string" ? { name: loc.item.name } : {}),
|
|
247
|
+
status: derived?.status ?? "planned",
|
|
248
|
+
phaseCount: derived?.phaseCount ?? 0,
|
|
249
|
+
phases,
|
|
250
|
+
rollup: rollupPhaseStatus(allTaskItems, vocabulary),
|
|
251
|
+
};
|
|
215
252
|
});
|
|
216
|
-
|
|
217
|
-
const edges = ctx.relations.filter((e) => e.relation_type === "phase_depends_on" &&
|
|
218
|
-
inRoadmap.has(endpointKey(e.parent)) &&
|
|
219
|
-
inRoadmap.has(endpointKey(e.child)));
|
|
220
|
-
const { order, cycles } = topoSort(roadmap.phases, (p) => p.id, (p) => edges.filter((e) => endpointKey(e.child) === p.id).map((e) => endpointKey(e.parent)));
|
|
221
|
-
return { roadmap, phases, phaseOrder: order, cycles, edges };
|
|
253
|
+
return { milestones, order, cycles, edges };
|
|
222
254
|
}
|
|
223
255
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
*
|
|
241
|
-
* display strings resolve via diagMessage (config.display_strings →
|
|
242
|
-
* embedded English fallback). The pi-context lens-validator dispatch
|
|
243
|
-
* (registered at module bottom) maps these to ContextValidationIssue-
|
|
244
|
-
* compatible records and merges them into validateContext's output.
|
|
256
|
+
* Validate the derived roadmap. Codes are opaque slugs; display strings
|
|
257
|
+
* resolve via diagMessage (config.display_strings → embedded English
|
|
258
|
+
* fallback). The pi-context lens-validator dispatch (registered at module
|
|
259
|
+
* bottom) contributes the error-code issues into validateContext's output.
|
|
260
|
+
*
|
|
261
|
+
* Codes:
|
|
262
|
+
* roadmap_precedes_endpoint_missing (error) — a milestone_precedes_milestone
|
|
263
|
+
* edge endpoint that is not a milestone-block item (absent OR wrong kind).
|
|
264
|
+
* roadmap_milestone_cycle (error) — a cycle in the precedes graph.
|
|
265
|
+
* roadmap_milestone_missing (error) — a phase_positioned_in_milestone edge
|
|
266
|
+
* whose child is not a known milestone.
|
|
267
|
+
* roadmap_status_unknown_value (warning) — a member phase whose task rollup
|
|
268
|
+
* buckets unknown with total > 0 (items lack a recognised status value).
|
|
269
|
+
* roadmap_milestone_isolated (info) — a milestone with zero precedes edges
|
|
270
|
+
* while at least one precedes edge exists elsewhere. Info NEVER flips
|
|
271
|
+
* status: status = invalid iff any error-code issue; warnings iff any
|
|
272
|
+
* warning-code issue; else clean.
|
|
245
273
|
*
|
|
246
|
-
*
|
|
247
|
-
* absence is not a defect).
|
|
274
|
+
* No config → { status: "clean", issues: [] } (nothing to validate).
|
|
248
275
|
*/
|
|
249
|
-
export function
|
|
276
|
+
export function validateRoadmap(cwd) {
|
|
250
277
|
const issues = [];
|
|
251
278
|
const ctx = loadContext(cwd);
|
|
252
279
|
if (!ctx.config)
|
|
253
280
|
return { status: "clean", issues: [] };
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
for (const roadmap of roadmaps) {
|
|
267
|
-
// roadmap.status — must be in known enum (warning when not).
|
|
268
|
-
if (roadmap.status !== undefined && !ROADMAP_STATUS_ENUM.has(roadmap.status)) {
|
|
269
|
-
issues.push({
|
|
270
|
-
code: "roadmap_status_unknown_value",
|
|
271
|
-
message: diagMessage(cwd, "roadmap_status_unknown_value", `Roadmap '${roadmap.id}' has unknown status '${roadmap.status}'. Known: ${Array.from(ROADMAP_STATUS_ENUM).join(", ")}`),
|
|
272
|
-
roadmap_id: roadmap.id,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
const inRoadmap = new Set(roadmap.phases.map((p) => p.id));
|
|
276
|
-
const milestoneIds = new Set((roadmap.milestones ?? []).map((m) => m.id));
|
|
277
|
-
// Per-phase checks: lens existence + composition cycle + status rollup
|
|
278
|
-
// for unknown-status warning + milestone validation (independent of
|
|
279
|
-
// lens resolution — milestone diagnostics surface even when the lens
|
|
280
|
-
// itself fails to load).
|
|
281
|
-
for (const phase of roadmap.phases) {
|
|
282
|
-
let lensResolved = false;
|
|
283
|
-
if (!lensIds.has(phase.lens)) {
|
|
281
|
+
const index = buildIdIndex(cwd);
|
|
282
|
+
const milestoneIds = new Set(index.items.filter((loc) => loc.block === "milestone").map((loc) => loc.id));
|
|
283
|
+
// Precedes-edge endpoint integrity — BOTH endpoints must be milestone-block
|
|
284
|
+
// items (an id resolving to another block is "wrong kind", equally missing).
|
|
285
|
+
// This is an orientation-independent both-endpoints check, so it needs no
|
|
286
|
+
// role_direction; it selects the relation by canonical_id only.
|
|
287
|
+
for (const e of ctx.relations) {
|
|
288
|
+
if (e.relation_type !== "milestone_precedes_milestone")
|
|
289
|
+
continue;
|
|
290
|
+
for (const endpoint of [e.parent, e.child]) {
|
|
291
|
+
const key = endpointKey(endpoint);
|
|
292
|
+
if (!milestoneIds.has(key)) {
|
|
284
293
|
issues.push({
|
|
285
|
-
code: "
|
|
286
|
-
message: diagMessage(cwd, "
|
|
287
|
-
|
|
288
|
-
phase_id: phase.id,
|
|
294
|
+
code: "roadmap_precedes_endpoint_missing",
|
|
295
|
+
message: diagMessage(cwd, "roadmap_precedes_endpoint_missing", `milestone_precedes_milestone edge {parent:${endpointKey(e.parent)}, child:${endpointKey(e.child)}} references '${key}' which is not a milestone-block item.`),
|
|
296
|
+
milestone_id: key,
|
|
289
297
|
});
|
|
290
298
|
}
|
|
291
|
-
else {
|
|
292
|
-
const lensView = loadLensView(cwd, phase.lens);
|
|
293
|
-
if ("error" in lensView) {
|
|
294
|
-
if (lensView.error.includes("composition_cycle_detected")) {
|
|
295
|
-
issues.push({
|
|
296
|
-
code: "roadmap_composition_cycle",
|
|
297
|
-
message: diagMessage(cwd, "roadmap_composition_cycle", `Phase '${phase.id}' in roadmap '${roadmap.id}' resolves to composition lens with cycle: ${lensView.error}`),
|
|
298
|
-
roadmap_id: roadmap.id,
|
|
299
|
-
phase_id: phase.id,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
lensResolved = true;
|
|
305
|
-
const status = rollupPhaseStatus(lensView.items, vocabulary);
|
|
306
|
-
if (status.bucket === "unknown" && status.total > 0) {
|
|
307
|
-
issues.push({
|
|
308
|
-
code: "roadmap_status_unknown_value",
|
|
309
|
-
message: diagMessage(cwd, "roadmap_status_unknown_value", `Phase '${phase.id}' in roadmap '${roadmap.id}' rolls up to bucket 'unknown' across ${status.total} item(s) — items lack a recognised status enum value.`),
|
|
310
|
-
roadmap_id: roadmap.id,
|
|
311
|
-
phase_id: phase.id,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Milestone evidence_block + evidence_query checks — independent
|
|
317
|
-
// of lens resolution outcome.
|
|
318
|
-
if (phase.milestone) {
|
|
319
|
-
const milestone = roadmap.milestones?.find((m) => m.id === phase.milestone);
|
|
320
|
-
if (milestone) {
|
|
321
|
-
if (milestone.evidence_block && !blockNames.has(milestone.evidence_block)) {
|
|
322
|
-
issues.push({
|
|
323
|
-
code: "roadmap_milestone_evidence_block_missing",
|
|
324
|
-
message: diagMessage(cwd, "roadmap_milestone_evidence_block_missing", `Milestone '${milestone.id}' in roadmap '${roadmap.id}' references evidence_block '${milestone.evidence_block}' which is not loaded.`),
|
|
325
|
-
roadmap_id: roadmap.id,
|
|
326
|
-
phase_id: phase.id,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
if (milestone.evidence_query !== undefined) {
|
|
330
|
-
const q = milestone.evidence_query;
|
|
331
|
-
const ok = q !== null &&
|
|
332
|
-
typeof q === "object" &&
|
|
333
|
-
!Array.isArray(q) &&
|
|
334
|
-
Object.values(q).every((v) => v === null || ["string", "number", "boolean"].includes(typeof v));
|
|
335
|
-
if (!ok) {
|
|
336
|
-
issues.push({
|
|
337
|
-
code: "roadmap_milestone_query_invalid",
|
|
338
|
-
message: diagMessage(cwd, "roadmap_milestone_query_invalid", `Milestone '${milestone.id}' in roadmap '${roadmap.id}' has invalid evidence_query — expected plain object with primitive values only.`),
|
|
339
|
-
roadmap_id: roadmap.id,
|
|
340
|
-
phase_id: phase.id,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
void lensResolved;
|
|
347
299
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
roadmap_id: roadmap.id,
|
|
365
|
-
phase_id: dangling,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
300
|
+
}
|
|
301
|
+
// Phase-membership integrity — a phase_positioned_in_milestone edge's CONTAINER
|
|
302
|
+
// (the milestone, at the relation's PRIMARY endpoint) must be a known milestone.
|
|
303
|
+
const phaseMembershipRel = "phase_positioned_in_milestone";
|
|
304
|
+
const phaseDir = roadmapRoleDirection(ctx.config, phaseMembershipRel, "as_child");
|
|
305
|
+
for (const e of ctx.relations) {
|
|
306
|
+
if (e.relation_type !== phaseMembershipRel)
|
|
307
|
+
continue;
|
|
308
|
+
const milestoneKey = endpointKey(primaryEndpoint(e, phaseDir));
|
|
309
|
+
if (!milestoneIds.has(milestoneKey)) {
|
|
310
|
+
issues.push({
|
|
311
|
+
code: "roadmap_milestone_missing",
|
|
312
|
+
message: diagMessage(cwd, "roadmap_milestone_missing", `phase_positioned_in_milestone edge {parent:${endpointKey(e.parent)}, child:${endpointKey(e.child)}} references milestone '${milestoneKey}' that is not declared in the milestone block.`),
|
|
313
|
+
milestone_id: milestoneKey,
|
|
314
|
+
phase_id: endpointKey(counterEndpoint(e, phaseDir)),
|
|
315
|
+
});
|
|
368
316
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
for (const cycle of cycles) {
|
|
317
|
+
}
|
|
318
|
+
// Derived-view checks (cycles + per-phase rollup + isolation) read the same
|
|
319
|
+
// derivation the load path produces.
|
|
320
|
+
const view = loadRoadmap(cwd);
|
|
321
|
+
if (!("error" in view)) {
|
|
322
|
+
for (const cycle of view.cycles) {
|
|
375
323
|
issues.push({
|
|
376
|
-
code: "
|
|
377
|
-
message: diagMessage(cwd, "
|
|
378
|
-
roadmap_id: roadmap.id,
|
|
324
|
+
code: "roadmap_milestone_cycle",
|
|
325
|
+
message: diagMessage(cwd, "roadmap_milestone_cycle", `Milestone cycle in milestone_precedes_milestone edges: ${cycle.join(" → ")}`),
|
|
379
326
|
cycle,
|
|
380
327
|
});
|
|
381
328
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
329
|
+
for (const m of view.milestones) {
|
|
330
|
+
for (const p of m.phases) {
|
|
331
|
+
if (p.rollup.bucket === "unknown" && p.rollup.total > 0) {
|
|
332
|
+
issues.push({
|
|
333
|
+
code: "roadmap_status_unknown_value",
|
|
334
|
+
message: diagMessage(cwd, "roadmap_status_unknown_value", `Phase '${p.id}' in milestone '${m.id}' rolls up to bucket 'unknown' across ${p.rollup.total} item(s) — items lack a recognised status enum value.`),
|
|
335
|
+
milestone_id: m.id,
|
|
336
|
+
phase_id: p.id,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (view.edges.length > 0) {
|
|
342
|
+
const connected = new Set();
|
|
343
|
+
for (const e of view.edges) {
|
|
344
|
+
connected.add(endpointKey(e.parent));
|
|
345
|
+
connected.add(endpointKey(e.child));
|
|
346
|
+
}
|
|
347
|
+
for (const m of view.milestones) {
|
|
348
|
+
if (!connected.has(m.id)) {
|
|
349
|
+
issues.push({
|
|
350
|
+
code: "roadmap_milestone_isolated",
|
|
351
|
+
message: diagMessage(cwd, "roadmap_milestone_isolated", `Milestone '${m.id}' participates in no milestone_precedes_milestone edge while other milestones are ordered.`),
|
|
352
|
+
milestone_id: m.id,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
385
357
|
}
|
|
358
|
+
// Info codes are excluded from status: invalid iff any error-code issue;
|
|
359
|
+
// warnings iff any warning-code issue; else clean.
|
|
386
360
|
let status = "clean";
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
361
|
+
if (issues.some((i) => ROADMAP_ERROR_CODES.has(i.code)))
|
|
362
|
+
status = "invalid";
|
|
363
|
+
else if (issues.some((i) => ROADMAP_WARNING_CODES.has(i.code)))
|
|
392
364
|
status = "warnings";
|
|
393
|
-
}
|
|
394
365
|
return { status, issues };
|
|
395
366
|
}
|
|
396
367
|
/**
|
|
397
|
-
* Render a loaded
|
|
398
|
-
* diagrams, NO graph syntax. Per-
|
|
399
|
-
* are sourced strictly from view.edges (sorted alphabetically; "—" when
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* naming consulted only for lens-target display labels; phase / roadmap
|
|
407
|
-
* ids render as-is.
|
|
368
|
+
* Render a loaded MilestoneRoadmapView as pure-textual markdown — NO mermaid,
|
|
369
|
+
* NO diagrams, NO graph syntax. Per-milestone **Preceded by:** adjacency lines
|
|
370
|
+
* are sourced strictly from view.edges (sorted alphabetically; "—" when the
|
|
371
|
+
* milestone has no incoming precedes edges). Cycle-participating milestones
|
|
372
|
+
* are surfaced under a separate "**Unordered (cycle participants):**" heading
|
|
373
|
+
* and a "**Cycles detected:**" line. The renderer never infers adjacency from
|
|
374
|
+
* `order` consecutive pairs (that pattern fabricates sibling edges in
|
|
375
|
+
* branching DAGs).
|
|
408
376
|
*/
|
|
409
|
-
export function renderRoadmap(view
|
|
377
|
+
export function renderRoadmap(view) {
|
|
410
378
|
const lines = [];
|
|
411
|
-
const
|
|
412
|
-
lines.push(
|
|
379
|
+
const byId = new Map(view.milestones.map((m) => [m.id, m]));
|
|
380
|
+
lines.push("# Roadmap (derived)");
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(`**Milestones:** ${view.milestones.length} | **Ordered:** ${view.order.length} | **Cycles:** ${view.cycles.length}`);
|
|
413
383
|
lines.push("");
|
|
414
|
-
lines.push(
|
|
415
|
-
lines.push(`**Phases:** ${r.phases.length} | **Ordered:** ${view.phaseOrder.length} | **Cycles:** ${view.cycles.length}`);
|
|
384
|
+
lines.push("## Milestone order");
|
|
416
385
|
lines.push("");
|
|
417
|
-
if (
|
|
418
|
-
lines.push(
|
|
386
|
+
if (view.milestones.length === 0) {
|
|
387
|
+
lines.push("(no milestones)");
|
|
419
388
|
lines.push("");
|
|
420
389
|
}
|
|
421
|
-
|
|
422
|
-
lines.push("");
|
|
423
|
-
const phaseById = new Map(r.phases.map((p) => [p.id, p]));
|
|
424
|
-
if (view.phaseOrder.length === 0) {
|
|
390
|
+
else if (view.order.length === 0) {
|
|
425
391
|
lines.push("(no acyclic ordering possible — see cycles below)");
|
|
426
392
|
lines.push("");
|
|
427
393
|
}
|
|
428
394
|
else {
|
|
429
|
-
view.
|
|
430
|
-
const
|
|
431
|
-
lines.push(`${idx + 1}. ${id} — ${
|
|
395
|
+
view.order.forEach((id, idx) => {
|
|
396
|
+
const m = byId.get(id);
|
|
397
|
+
lines.push(`${idx + 1}. ${id} — ${m?.name ?? "(unnamed)"} [${m?.status ?? "planned"}]`);
|
|
432
398
|
});
|
|
433
399
|
lines.push("");
|
|
434
400
|
}
|
|
435
|
-
const orderedSet = new Set(view.
|
|
436
|
-
const cycleOnly =
|
|
401
|
+
const orderedSet = new Set(view.order);
|
|
402
|
+
const cycleOnly = view.milestones.filter((m) => !orderedSet.has(m.id));
|
|
437
403
|
if (cycleOnly.length > 0) {
|
|
438
|
-
lines.push("**Unordered (cycle
|
|
439
|
-
for (const
|
|
440
|
-
lines.push(`- ${
|
|
404
|
+
lines.push("**Unordered (cycle participants):**");
|
|
405
|
+
for (const m of cycleOnly) {
|
|
406
|
+
lines.push(`- ${m.id} — ${m.name ?? "(unnamed)"}`);
|
|
441
407
|
}
|
|
442
408
|
lines.push("");
|
|
443
409
|
}
|
|
@@ -446,98 +412,68 @@ export function renderRoadmap(view, naming) {
|
|
|
446
412
|
lines.push(`**Cycles detected:** ${rendered}`);
|
|
447
413
|
lines.push("");
|
|
448
414
|
}
|
|
449
|
-
|
|
415
|
+
if (view.milestones.length === 0) {
|
|
416
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
417
|
+
}
|
|
418
|
+
lines.push("## Milestones");
|
|
450
419
|
lines.push("");
|
|
451
|
-
// Render in
|
|
452
|
-
const
|
|
453
|
-
const renderOrder = [];
|
|
454
|
-
for (const id of view.phaseOrder)
|
|
455
|
-
renderOrder.push(id);
|
|
456
|
-
for (const p of cycleOnly)
|
|
457
|
-
renderOrder.push(p.id);
|
|
420
|
+
// Render in topo order first, then any cycle-only milestones (input order).
|
|
421
|
+
const renderOrder = [...view.order, ...cycleOnly.map((m) => m.id)];
|
|
458
422
|
for (const id of renderOrder) {
|
|
459
|
-
const
|
|
460
|
-
if (!
|
|
423
|
+
const m = byId.get(id);
|
|
424
|
+
if (!m)
|
|
461
425
|
continue;
|
|
462
|
-
|
|
463
|
-
lines.push(`### ${p.name} (${p.id}) [${pv.status.bucket}]`);
|
|
426
|
+
lines.push(`### ${m.name ?? "(unnamed)"} (${m.id}) [${m.status}]`);
|
|
464
427
|
lines.push("");
|
|
465
|
-
const lensTargetLabel = (() => {
|
|
466
|
-
if (!("error" in pv.lensView)) {
|
|
467
|
-
const lens = pv.lensView.lens;
|
|
468
|
-
if (lens.kind === "composition") {
|
|
469
|
-
return `${(lens.targets ?? []).map((t) => naming?.[t] ?? t).join(", ")} (composition)`;
|
|
470
|
-
}
|
|
471
|
-
if (lens.target)
|
|
472
|
-
return naming?.[lens.target] ?? lens.target;
|
|
473
|
-
}
|
|
474
|
-
return p.lens;
|
|
475
|
-
})();
|
|
476
|
-
lines.push(`**Lens:** ${p.lens}${lensTargetLabel !== p.lens ? ` (${lensTargetLabel})` : ""}`);
|
|
477
428
|
const incoming = view.edges
|
|
478
|
-
.filter((e) => endpointKey(e.child) ===
|
|
429
|
+
.filter((e) => endpointKey(e.child) === m.id)
|
|
479
430
|
.map((e) => endpointKey(e.parent))
|
|
480
431
|
.sort();
|
|
481
|
-
lines.push(`**
|
|
482
|
-
const c =
|
|
483
|
-
lines.push(`**
|
|
432
|
+
lines.push(`**Preceded by:** ${incoming.length > 0 ? incoming.join(", ") : "—"}`);
|
|
433
|
+
const c = m.rollup.counts;
|
|
434
|
+
lines.push(`**Rollup:** complete=${c.complete} in_progress=${c.in_progress} blocked=${c.blocked} todo=${c.todo} unknown=${c.unknown} (total=${m.rollup.total})`);
|
|
484
435
|
lines.push("");
|
|
485
|
-
|
|
486
|
-
lines.push(
|
|
436
|
+
for (const p of m.phases) {
|
|
437
|
+
lines.push(`#### ${p.name ?? "(unnamed)"} (${p.id}) [${p.status ?? "(unspecified)"}]`);
|
|
487
438
|
lines.push("");
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
lines.push(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
lines.push(`| ${item.id} | ${status} | ${titleStr} |`);
|
|
439
|
+
if (p.tasks.length === 0) {
|
|
440
|
+
lines.push("(no tasks)");
|
|
441
|
+
lines.push("");
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
lines.push("| Task | Status |");
|
|
445
|
+
lines.push("|------|--------|");
|
|
446
|
+
for (const t of p.tasks) {
|
|
447
|
+
lines.push(`| ${t.title ?? t.id} | ${t.status ?? ""} |`);
|
|
498
448
|
}
|
|
499
449
|
lines.push("");
|
|
500
450
|
}
|
|
501
451
|
}
|
|
502
|
-
if (pv.milestone) {
|
|
503
|
-
const verdict = pv.milestoneSatisfied ? "satisfied" : "not yet satisfied";
|
|
504
|
-
lines.push(`**Milestone:** ${pv.milestone.id} — ${pv.milestone.name} — ${verdict}`);
|
|
505
|
-
lines.push("");
|
|
506
|
-
}
|
|
507
|
-
if (p.exit_criteria && p.exit_criteria.length > 0) {
|
|
508
|
-
lines.push("**Exit criteria:**");
|
|
509
|
-
for (const crit of p.exit_criteria)
|
|
510
|
-
lines.push(`- ${crit}`);
|
|
511
|
-
lines.push("");
|
|
512
|
-
}
|
|
513
452
|
}
|
|
514
453
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
515
454
|
}
|
|
516
|
-
// ── Lens-validator dispatch registration
|
|
455
|
+
// ── Lens-validator dispatch registration ─────────────────────────────────────
|
|
517
456
|
//
|
|
518
457
|
// Module-init side-effect: validateContext in context-sdk.ts iterates all
|
|
519
458
|
// registered lens-validators and merges their issues into the project-
|
|
520
459
|
// validation result. roadmap-plan registers itself here so import of this
|
|
521
460
|
// module from index.ts wires the dispatch automatically — no hardcoded
|
|
522
|
-
// import in context-sdk.ts.
|
|
461
|
+
// import in context-sdk.ts. Only error-code issues contribute to the merge:
|
|
462
|
+
// warnings and info stay on the context-roadmap-validate surface.
|
|
523
463
|
registerLensValidator({
|
|
524
464
|
name: "roadmap",
|
|
525
465
|
validate: (cwd) => {
|
|
526
|
-
const result =
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
"roadmap_phase_cycle",
|
|
531
|
-
"roadmap_composition_cycle",
|
|
532
|
-
]);
|
|
533
|
-
const issues = result.issues.map((ri) => ({
|
|
466
|
+
const result = validateRoadmap(cwd);
|
|
467
|
+
const issues = result.issues
|
|
468
|
+
.filter((ri) => ROADMAP_ERROR_CODES.has(ri.code))
|
|
469
|
+
.map((ri) => ({
|
|
534
470
|
code: ri.code,
|
|
535
|
-
severity:
|
|
471
|
+
severity: "error",
|
|
536
472
|
message: ri.message,
|
|
537
|
-
block:
|
|
473
|
+
block: "milestone",
|
|
538
474
|
...(ri.phase_id ? { field: `phase.${ri.phase_id}` } : {}),
|
|
539
475
|
}));
|
|
540
|
-
const status = issues.length === 0 ? "clean" :
|
|
476
|
+
const status = issues.length === 0 ? "clean" : "invalid";
|
|
541
477
|
return { status, issues };
|
|
542
478
|
},
|
|
543
479
|
});
|