@infinitedusky/indusk-mcp 1.24.5 → 1.25.1
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.
|
@@ -14,6 +14,15 @@ export interface ValidateTrajectoryOptions {
|
|
|
14
14
|
* check at apps/indusk-mcp/hooks/validate-impl-structure.js.
|
|
15
15
|
*/
|
|
16
16
|
rationaleRequired?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The phase number that counts as "writable today against the current stack."
|
|
19
|
+
* Trajectory rows whose `Writable at` is ≤ baseline are exempt from the
|
|
20
|
+
* rationale-completeness rule. Defaults to 0 (the original behavior:
|
|
21
|
+
* Phase 0 rows are exempt). Plans where Phase 1 IS the enabling work
|
|
22
|
+
* (refactors, schema migrations, scaffolding) set this to 1 so rows
|
|
23
|
+
* authored at Phase 1 don't require justification entries.
|
|
24
|
+
*/
|
|
25
|
+
rationaleBaseline?: number;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Rule 1: Every impl document must have a `## Test Trajectory` section.
|
|
@@ -39,23 +48,27 @@ export declare function validateTemporalCoherence(trajectory: Trajectory): Valid
|
|
|
39
48
|
export declare function validateDeferredCompleteness(trajectory: Trajectory): ValidationError[];
|
|
40
49
|
/**
|
|
41
50
|
* Rule 5: When the impl frontmatter sets `rationale: required`, every
|
|
42
|
-
* trajectory row whose `Writable at` is later than
|
|
43
|
-
*
|
|
51
|
+
* trajectory row whose `Writable at` is later than the configured baseline
|
|
52
|
+
* (default Phase 0) must have an entry in the `### Trajectory Rationale`
|
|
53
|
+
* subsection.
|
|
44
54
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
55
|
+
* The baseline names the phase that counts as "writable today against the
|
|
56
|
+
* current stack" for this plan. Default 0 — Phase 0 rows are exempt because
|
|
57
|
+
* they're writable before any plan code lands. Plans where Phase 1 IS the
|
|
58
|
+
* enabling work (refactors, schema migrations, scaffolding) can declare
|
|
59
|
+
* `rationale_baseline: 1` in frontmatter so Phase 1 rows are exempt too.
|
|
49
60
|
*
|
|
50
|
-
* If no row needs a rationale (every row is
|
|
51
|
-
* is optional. If any row is
|
|
52
|
-
* an entry for every
|
|
53
|
-
* trajectory) are always flagged.
|
|
61
|
+
* If no row needs a rationale (every row is ≤ baseline), the subsection
|
|
62
|
+
* itself is optional. If any row is later than baseline, the subsection
|
|
63
|
+
* must exist and contain an entry for every such row. Stale entries
|
|
64
|
+
* (entries for IDs not in the trajectory) are always flagged.
|
|
54
65
|
*
|
|
55
66
|
* Mirrors `validateRationaleCompleteness` in
|
|
56
67
|
* `.claude/hooks/validate-impl-structure.js`.
|
|
57
68
|
*/
|
|
58
|
-
export declare function validateRationaleCompleteness(body: string, trajectory: Trajectory
|
|
69
|
+
export declare function validateRationaleCompleteness(body: string, trajectory: Trajectory, options?: {
|
|
70
|
+
baseline?: number;
|
|
71
|
+
}): ValidationError[];
|
|
59
72
|
/**
|
|
60
73
|
* Run all trajectory validation rules against an impl body. The body is the
|
|
61
74
|
* markdown content after the frontmatter — pass the output of `gray-matter`
|
|
@@ -185,31 +185,34 @@ export function validateDeferredCompleteness(trajectory) {
|
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
187
|
* Rule 5: When the impl frontmatter sets `rationale: required`, every
|
|
188
|
-
* trajectory row whose `Writable at` is later than
|
|
189
|
-
*
|
|
188
|
+
* trajectory row whose `Writable at` is later than the configured baseline
|
|
189
|
+
* (default Phase 0) must have an entry in the `### Trajectory Rationale`
|
|
190
|
+
* subsection.
|
|
190
191
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
192
|
+
* The baseline names the phase that counts as "writable today against the
|
|
193
|
+
* current stack" for this plan. Default 0 — Phase 0 rows are exempt because
|
|
194
|
+
* they're writable before any plan code lands. Plans where Phase 1 IS the
|
|
195
|
+
* enabling work (refactors, schema migrations, scaffolding) can declare
|
|
196
|
+
* `rationale_baseline: 1` in frontmatter so Phase 1 rows are exempt too.
|
|
195
197
|
*
|
|
196
|
-
* If no row needs a rationale (every row is
|
|
197
|
-
* is optional. If any row is
|
|
198
|
-
* an entry for every
|
|
199
|
-
* trajectory) are always flagged.
|
|
198
|
+
* If no row needs a rationale (every row is ≤ baseline), the subsection
|
|
199
|
+
* itself is optional. If any row is later than baseline, the subsection
|
|
200
|
+
* must exist and contain an entry for every such row. Stale entries
|
|
201
|
+
* (entries for IDs not in the trajectory) are always flagged.
|
|
200
202
|
*
|
|
201
203
|
* Mirrors `validateRationaleCompleteness` in
|
|
202
204
|
* `.claude/hooks/validate-impl-structure.js`.
|
|
203
205
|
*/
|
|
204
|
-
export function validateRationaleCompleteness(body, trajectory) {
|
|
206
|
+
export function validateRationaleCompleteness(body, trajectory, options = {}) {
|
|
205
207
|
const errors = [];
|
|
206
|
-
const
|
|
208
|
+
const baseline = Number.isFinite(options.baseline) ? Number(options.baseline) : 0;
|
|
209
|
+
const rowsNeedingRationale = trajectory.rows.filter((r) => Number.isFinite(r.writableAt) && r.writableAt > baseline);
|
|
207
210
|
const hasSubsection = /^###\s+Trajectory Rationale\b/m.test(body);
|
|
208
211
|
const rationaleIds = hasSubsection ? parseRationaleBlock(body) : new Set();
|
|
209
212
|
if (rowsNeedingRationale.length > 0 && !hasSubsection) {
|
|
210
213
|
errors.push({
|
|
211
214
|
rule: "rationale-completeness",
|
|
212
|
-
message: `\`rationale: required\` is set and ${rowsNeedingRationale.length} trajectory row(s) have \`Writable at\` later than Phase
|
|
215
|
+
message: `\`rationale: required\` is set and ${rowsNeedingRationale.length} trajectory row(s) have \`Writable at\` later than Phase ${baseline}, but the impl is missing the \`### Trajectory Rationale\` subsection. Rows at or below the baseline don't need rationale; rows where authoring waits on later plan code do — add an entry for ${rowsNeedingRationale.map((r) => r.id).join(", ")}.`,
|
|
213
216
|
});
|
|
214
217
|
// Even without the subsection, fall through to also check for stale entries
|
|
215
218
|
// (there are none in this case, but the structure is symmetric).
|
|
@@ -222,7 +225,7 @@ export function validateRationaleCompleteness(body, trajectory) {
|
|
|
222
225
|
if (missing.length > 0 && hasSubsection) {
|
|
223
226
|
errors.push({
|
|
224
227
|
rule: "rationale-completeness",
|
|
225
|
-
message: `Trajectory rows with \`Writable at\` later than Phase
|
|
228
|
+
message: `Trajectory rows with \`Writable at\` later than Phase ${baseline} missing from \`### Trajectory Rationale\`: ${missing.join(", ")}. Every row whose authoring waits on later plan code needs a \`- **TN** \`Writable at: Phase N\` — {reason}\` entry. Rows at or below the baseline (Phase ${baseline}) do not need rationale.`,
|
|
226
229
|
});
|
|
227
230
|
}
|
|
228
231
|
const knownIds = new Set(trajectory.rows.map((r) => r.id));
|
|
@@ -278,7 +281,9 @@ export function validateTrajectory(body, options = {}) {
|
|
|
278
281
|
...validateDeferredCompleteness(trajectory),
|
|
279
282
|
];
|
|
280
283
|
if (options.rationaleRequired) {
|
|
281
|
-
errors.push(...validateRationaleCompleteness(body, trajectory
|
|
284
|
+
errors.push(...validateRationaleCompleteness(body, trajectory, {
|
|
285
|
+
baseline: options.rationaleBaseline,
|
|
286
|
+
}));
|
|
282
287
|
}
|
|
283
288
|
return errors;
|
|
284
289
|
}
|
|
@@ -312,9 +312,19 @@ const trajectoryRequiredFrontmatter = /trajectory:\s*required/.test(frontmatter)
|
|
|
312
312
|
const hasTrajectoryHeading = /^##\s+Test Trajectory\b/m.test(body);
|
|
313
313
|
const trajectoryValidationEnabled = trajectoryRequiredFrontmatter || hasTrajectoryHeading;
|
|
314
314
|
const rationaleRequiredFrontmatter = /rationale:\s*required/.test(frontmatter);
|
|
315
|
+
// Anchor to start-of-line within frontmatter (m flag) so the key is only matched
|
|
316
|
+
// when it appears as a top-level YAML key — NOT when its name appears as a
|
|
317
|
+
// substring inside a quoted value (e.g., a `title:` mentioning the key).
|
|
318
|
+
// Surfaced by /falsify hypothesis 1: a documentation plan whose title contained
|
|
319
|
+
// the literal `rationale_baseline: 1` silently inherited that baseline from the
|
|
320
|
+
// title's substring. See .indusk/planning/rationale-baseline-frontmatter/falsification.md.
|
|
321
|
+
const rationaleBaselineMatch = frontmatter.match(/^rationale_baseline:\s*(\d+)/m);
|
|
322
|
+
const rationaleBaseline = rationaleBaselineMatch
|
|
323
|
+
? Number.parseInt(rationaleBaselineMatch[1], 10)
|
|
324
|
+
: 0;
|
|
315
325
|
|
|
316
326
|
if (trajectoryValidationEnabled) {
|
|
317
|
-
const trajectoryErrors = validateTrajectory(body, rationaleRequiredFrontmatter);
|
|
327
|
+
const trajectoryErrors = validateTrajectory(body, rationaleRequiredFrontmatter, rationaleBaseline);
|
|
318
328
|
if (trajectoryErrors.length > 0) {
|
|
319
329
|
process.stderr.write(
|
|
320
330
|
`Test Trajectory validation failed (policy: ${gatePolicy}):\n${trajectoryErrors.map((e) => ` [${e.rule}] ${e.message}`).join("\n")}\n\nSee .indusk/planning/tests-first-planning/adr.md Sections 3-6 for the Test Trajectory shape and validator rules.\n`,
|
|
@@ -347,7 +357,7 @@ process.exit(0);
|
|
|
347
357
|
// apps/indusk-mcp/src/lib/trajectory/validator.ts and parser.ts)
|
|
348
358
|
// ------------------------------------------------------------------
|
|
349
359
|
|
|
350
|
-
function validateTrajectory(implBody, rationaleRequired) {
|
|
360
|
+
function validateTrajectory(implBody, rationaleRequired, rationaleBaseline = 0) {
|
|
351
361
|
const errors = [];
|
|
352
362
|
|
|
353
363
|
// Rule 1: trajectory presence
|
|
@@ -365,7 +375,7 @@ function validateTrajectory(implBody, rationaleRequired) {
|
|
|
365
375
|
errors.push(...validateTemporalCoherence(trajectory));
|
|
366
376
|
errors.push(...validateDeferredCompleteness(trajectory));
|
|
367
377
|
if (rationaleRequired) {
|
|
368
|
-
errors.push(...validateRationaleCompleteness(implBody, trajectory));
|
|
378
|
+
errors.push(...validateRationaleCompleteness(implBody, trajectory, rationaleBaseline));
|
|
369
379
|
}
|
|
370
380
|
return errors;
|
|
371
381
|
}
|
|
@@ -646,11 +656,12 @@ function validateDeferredCompleteness(trajectory) {
|
|
|
646
656
|
// Read the entries together: shared weak excuses signal over-sequencing.
|
|
647
657
|
// ------------------------------------------------------------------
|
|
648
658
|
|
|
649
|
-
function validateRationaleCompleteness(implBody, trajectory) {
|
|
659
|
+
function validateRationaleCompleteness(implBody, trajectory, baseline = 0) {
|
|
650
660
|
const errors = [];
|
|
661
|
+
const baselineNum = Number.isFinite(baseline) ? Number(baseline) : 0;
|
|
651
662
|
|
|
652
663
|
const rowsNeedingRationale = trajectory.rows.filter(
|
|
653
|
-
(r) => Number.isFinite(r.writableAt) && r.writableAt >
|
|
664
|
+
(r) => Number.isFinite(r.writableAt) && r.writableAt > baselineNum,
|
|
654
665
|
);
|
|
655
666
|
const hasSubsection = /^###\s+Trajectory Rationale\b/m.test(implBody);
|
|
656
667
|
const rationaleIds = hasSubsection ? parseRationaleBlock(implBody) : new Set();
|
|
@@ -658,7 +669,7 @@ function validateRationaleCompleteness(implBody, trajectory) {
|
|
|
658
669
|
if (rowsNeedingRationale.length > 0 && !hasSubsection) {
|
|
659
670
|
errors.push({
|
|
660
671
|
rule: "rationale-completeness",
|
|
661
|
-
message: `\`rationale: required\` is set and ${rowsNeedingRationale.length} trajectory row(s) have \`Writable at\` later than Phase
|
|
672
|
+
message: `\`rationale: required\` is set and ${rowsNeedingRationale.length} trajectory row(s) have \`Writable at\` later than Phase ${baselineNum}, but the impl is missing the \`### Trajectory Rationale\` subsection. Rows at or below the baseline don't need rationale; rows where authoring waits on later plan code do — add an entry for ${rowsNeedingRationale.map((r) => r.id).join(", ")}.`,
|
|
662
673
|
});
|
|
663
674
|
}
|
|
664
675
|
|
|
@@ -670,7 +681,7 @@ function validateRationaleCompleteness(implBody, trajectory) {
|
|
|
670
681
|
if (missing.length > 0 && hasSubsection) {
|
|
671
682
|
errors.push({
|
|
672
683
|
rule: "rationale-completeness",
|
|
673
|
-
message: `Trajectory rows with \`Writable at\` later than Phase
|
|
684
|
+
message: `Trajectory rows with \`Writable at\` later than Phase ${baselineNum} missing from \`### Trajectory Rationale\`: ${missing.join(", ")}. Every row whose authoring waits on later plan code needs a \`- **TN** \`Writable at: Phase N\` — {reason}\` entry. Rows at or below the baseline (Phase ${baselineNum}) do not need rationale.`,
|
|
674
685
|
});
|
|
675
686
|
}
|
|
676
687
|
|