@infinitedusky/indusk-mcp 1.19.1 → 1.21.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.
@@ -311,9 +311,10 @@ for (const phase of phases) {
311
311
  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
+ const rationaleRequiredFrontmatter = /rationale:\s*required/.test(frontmatter);
314
315
 
315
316
  if (trajectoryValidationEnabled) {
316
- const trajectoryErrors = validateTrajectory(body);
317
+ const trajectoryErrors = validateTrajectory(body, rationaleRequiredFrontmatter);
317
318
  if (trajectoryErrors.length > 0) {
318
319
  process.stderr.write(
319
320
  `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`,
@@ -346,7 +347,7 @@ process.exit(0);
346
347
  // apps/indusk-mcp/src/lib/trajectory/validator.ts and parser.ts)
347
348
  // ------------------------------------------------------------------
348
349
 
349
- function validateTrajectory(implBody) {
350
+ function validateTrajectory(implBody, rationaleRequired) {
350
351
  const errors = [];
351
352
 
352
353
  // Rule 1: trajectory presence
@@ -363,6 +364,9 @@ function validateTrajectory(implBody) {
363
364
  errors.push(...validateCrossReferenceIntegrity(implBody, trajectory));
364
365
  errors.push(...validateTemporalCoherence(trajectory));
365
366
  errors.push(...validateDeferredCompleteness(trajectory));
367
+ if (rationaleRequired) {
368
+ errors.push(...validateRationaleCompleteness(implBody, trajectory));
369
+ }
366
370
  return errors;
367
371
  }
368
372
 
@@ -632,3 +636,72 @@ function validateDeferredCompleteness(trajectory) {
632
636
  }
633
637
  return errors;
634
638
  }
639
+
640
+ // ------------------------------------------------------------------
641
+ // Rationale validation (earliest-writable discipline)
642
+ //
643
+ // When frontmatter has `rationale: required`, the impl must contain a
644
+ // `### Trajectory Rationale` subsection with an entry per trajectory row.
645
+ // Each entry names what prevents authoring the test at Phase 0 (pre-plan).
646
+ // Read the entries together: shared weak excuses signal over-sequencing.
647
+ // ------------------------------------------------------------------
648
+
649
+ function validateRationaleCompleteness(implBody, trajectory) {
650
+ const errors = [];
651
+
652
+ const rowsNeedingRationale = trajectory.rows.filter(
653
+ (r) => Number.isFinite(r.writableAt) && r.writableAt > 0,
654
+ );
655
+ const hasSubsection = /^###\s+Trajectory Rationale\b/m.test(implBody);
656
+ const rationaleIds = hasSubsection ? parseRationaleBlock(implBody) : new Set();
657
+
658
+ if (rowsNeedingRationale.length > 0 && !hasSubsection) {
659
+ errors.push({
660
+ rule: "rationale-completeness",
661
+ message: `\`rationale: required\` is set and ${rowsNeedingRationale.length} trajectory row(s) have \`Writable at\` later than Phase 0, but the impl is missing the \`### Trajectory Rationale\` subsection. Phase 0 rows don't need rationale; rows where authoring waits on plan code do — add an entry for ${rowsNeedingRationale.map((r) => r.id).join(", ")}.`,
662
+ });
663
+ }
664
+
665
+ const missing = [];
666
+ for (const row of rowsNeedingRationale) {
667
+ if (!rationaleIds.has(row.id)) missing.push(row.id);
668
+ }
669
+
670
+ if (missing.length > 0 && hasSubsection) {
671
+ errors.push({
672
+ rule: "rationale-completeness",
673
+ message: `Trajectory rows with \`Writable at\` later than Phase 0 missing from \`### Trajectory Rationale\`: ${missing.join(", ")}. Every row whose authoring waits on plan code needs a \`- **TN** \`Writable at: Phase N\` — {reason}\` entry. Phase 0 rows (writable today against the current stack) do not need rationale.`,
674
+ });
675
+ }
676
+
677
+ const extra = [...rationaleIds].filter((id) => !trajectory.rows.some((r) => r.id === id));
678
+ if (extra.length > 0) {
679
+ errors.push({
680
+ rule: "rationale-completeness",
681
+ message: `\`### Trajectory Rationale\` contains entries for IDs not present in the trajectory table: ${extra.join(", ")}. Remove the stale entries or add the missing trajectory rows.`,
682
+ });
683
+ }
684
+
685
+ return errors;
686
+ }
687
+
688
+ function parseRationaleBlock(implBody) {
689
+ const lines = implBody.split("\n");
690
+ const ids = new Set();
691
+ let inRationale = false;
692
+
693
+ for (const line of lines) {
694
+ if (/^###\s+Trajectory Rationale\b/.test(line)) {
695
+ inRationale = true;
696
+ continue;
697
+ }
698
+ if (!inRationale) continue;
699
+ // Break on next heading of depth 1-3 (new section starts)
700
+ if (/^#{1,3}\s+/.test(line) && !/^###\s+Trajectory Rationale\b/.test(line)) break;
701
+ // Match `- **TN**` at the start of a rationale entry
702
+ const match = line.match(/^-\s+\*\*(T\d+)\*\*/);
703
+ if (match) ids.add(match[1]);
704
+ }
705
+
706
+ return ids;
707
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.19.1",
3
+ "version": "1.21.0",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
package/skills/planner.md CHANGED
@@ -100,6 +100,43 @@ Workflow templates are in `templates/workflows/` in the package. They describe w
100
100
 
101
101
  **Author the Test Trajectory first.** Every new impl opens with a `## Test Trajectory` table (after `## Boundary Map`, before `## Checklist`) that enumerates the tests the plan commits to. Columns: `ID | Asserts | Writable at | Passes at | State` (plus optional `Kind`, `Scope`). Walk the ADR's Decision section — for each decision, ask "what test would prove this works?" and add a row. Then walk each planned phase and ask "what becomes writable at this phase, and what flips to passing?" Every phase's Verification block references test IDs from the trajectory rather than restating the checks.
102
102
 
103
+ **Writable at is the earliest possible phase, not the fix phase.** The rule: *if it is possible to write a test, write it — then let it pass when it will.* The validator only enforces `Writable at ≤ Passes at` (a floor); the real discipline is `Writable at = earliest feasible phase`. A test authored in the same phase as its fix is a rubber stamp — nothing proves intermediate phases didn't break it or fix it by accident. A test that goes red early and stays red through intermediate phases until its fix lands is a live tripwire: any intermediate phase that turns it green prematurely signals unexpected coupling; any intermediate phase that breaks an unrelated passing test signals regression.
104
+
105
+ Honest shapes:
106
+ - **Regression tests for reported bugs**: `Writable at: Phase 0` (the stack runs, the bug is reproducible today, no plan code needed to author). Passes at = the phase that lands the fix.
107
+ - **End-to-end scenarios via HTTP/WS**: `Writable at: Phase 0` if the test can be a script hitting current endpoints (404 today is real-red). Passes at = the phase that closes the last gap. Only move later if authoring requires a not-yet-existing TypeScript symbol or constructor signature.
108
+ - **Reconstruction / persistence tests**: `Writable at: Phase 0` if the test is a "restart-and-check" script (today fails because state doesn't persist, which is real-red). Move later only if the assertion references a not-yet-existing symbol.
109
+ - **Unit tests for new code**: `Writable at = Passes at` is legitimate when the test's subject is a TypeScript symbol (schema file, new function, new enum value) introduced in that phase — the test file would not compile today.
110
+ - **Grep-the-thing-is-gone tests**: `Writable at: Phase 0` (the old identifier exists today; the grep finds it, which is the red state). Passes at = the phase that removes the identifier.
111
+
112
+ Challenge each row before you write it down: *"could this test be authored earlier than the phase that makes it pass?"* If yes, `Writable at` must point to that earlier phase. The Writable-phase's Verification block gains a `(write red)` item that commits the test against the current implementation and asserts the expected failure symptom; the Passes-phase's Verification block keeps its `(goes green)` item. Both reference the same test ID — the validator accepts multiple phase references to one trajectory row.
113
+
114
+ **Phase 0 is the default; rationale is required only for Phase 1+ rows.** Every new impl sets `rationale: required` in its frontmatter. The `### Trajectory Rationale` subsection (placed after `### Deferred Verification`) is required ONLY when at least one trajectory row has `Writable at` later than Phase 0. Phase 0 means "writable today against the current stack, before any plan code lands" — it's the default and needs no justification. We only require rationale when a test will be authored AFTER some plan implementation has happened (Writable at: Phase 1+). This keeps the subsection from filling with "trivially writable today" boilerplate when most rows are correctly Phase 0.
115
+
116
+ The `validate-impl-structure.js` hook enforces completeness: every Phase 1+ T-ID must appear as a `- **TN** \`Writable at: Phase N\` — {reason}` entry, the subsection itself must exist when any Phase 1+ row exists, and stale entries (entries for IDs not in the trajectory table) are flagged.
117
+
118
+ Entry shape: `- **TN** \`Writable at: Phase N\` — {one-sentence reason}`. Examples:
119
+ - `- **T22** \`Writable at: Phase 0\` — Bug is reproducible today against the running stack; test is authorable against current behavior and fails red.` *(no rationale entry needed; included here only as a reminder of the Phase 0 default)*
120
+ - `- **T14** \`Writable at: Phase 5\` — Subject is the zod schema file authored in Phase 5; no import target exists before then.` *(needs rationale)*
121
+ - `- **T20** \`Writable at: Phase 6\` — Test constructs PokerV2Room with a settings argument; the constructor signature gains the settings parameter in Phase 6, so TypeScript rejects the test source today.` *(needs rationale)*
122
+
123
+ **The rationale-quality test:** *Does this rationale describe a compile error against today's symbols, or does it describe an uninteresting failure mode?* If the latter, the row is a rubber-stamp — move it to Phase 1.
124
+
125
+ - **Legitimate `Writable > Phase 1` (compile error against today's symbols):**
126
+ - Test imports a not-yet-exported TypeScript symbol — `import { pokerTableSettingsSchema } from "@numero/types"` when the export doesn't exist. The import line is a compile error; the test file cannot be authored.
127
+ - Test constructs an object using a constructor signature that doesn't exist — `new PokerV2Room({ settings: {...} })` when the constructor doesn't take `settings`. TypeScript rejects.
128
+ - Test asserts against an enum value that doesn't exist — `expect(result.phase).toBe(GamePhase.CollectingBlinds)` when `CollectingBlinds` isn't in the enum.
129
+ - **Rubber-stamp `Writable > Phase 1` (red for an uninteresting reason — move to Phase 1):**
130
+ - "Assertion checks for error code `X` which is introduced in Phase N." → String comparison. Authorable today; fails because today's response is silent-swallow or a different error code. Stays red until the convention lands.
131
+ - "Endpoint doesn't exist yet." → HTTP request returns 404. Authorable today; 404-red is real-red.
132
+ - "Column doesn't exist yet." → SQL query errors. Authorable today; query-error-red is real-red.
133
+ - "Reconstruction code doesn't read from this column yet." → Restart-and-check script. Authorable today; whatever signal emerges is real.
134
+ - "Migration script doesn't exist yet." → Migration runner returns "migration NNNN not found." Authorable today.
135
+
136
+ The line is *can the test source code be authored today*, not *would it fail for a satisfying reason*. Red-for-uninteresting-reason is the whole point of `Writable at = Phase 1`: the test stays red through every intermediate phase, and any phase that turns it green prematurely or breaks an unrelated test surfaces a regression you'd otherwise miss.
137
+
138
+ Why it matters: read the rationales as a set after authoring. If multiple rows share the same weak excuse ("depends on the fix landing", "endpoint doesn't exist yet", "error code not defined yet"), the plan is over-sequenced and those tests should move earlier. The rationale subsection is the discipline tool — the validator enforces its presence; the human judgment is whether each rationale describes a real compile error or a rubber-stamped failure mode.
139
+
103
140
  **Trajectory sizing:** 3–5 tests for a bugfix or small feature, 10–25 for a multi-phase infrastructure plan. Prefer one high-level property test over five example tests where possible. If your trajectory has more rows than lines of new code, the plan is over-specified — consolidate. If it has fewer than one row per phase, you probably have untested phases — add rows or declare `(no tests flip at this phase — reason: {schema-only|delete|refactor|infra})` in the phase's Verification.
104
141
 
105
142
  **Declare untestable items explicitly.** If a plan includes something that genuinely cannot be tested (LLM quality, paid external integrations, UX judgment), add a `### Deferred Verification` subsection below the trajectory table. Every deferred row requires three fields: `reason:` (why not testable here), `would require:` (what would unlock a proper test), and `mitigation:` (compensating control — alert, scheduled review, downstream plan, canary). Missing any field is a write-time error. If you can't name a mitigation, that's a signal: either reshape the plan so the capability becomes testable, or scope it out.
@@ -202,6 +239,12 @@ status: proposed | accepted | deprecated | superseded | abandoned
202
239
 
203
240
  # {Title}
204
241
 
242
+ ## Goal
243
+
244
+ **{One sentence. The headline outcome, in plain language. What will be true when this ADR's decisions ship that isn't true today.}**
245
+
246
+ {One short paragraph — 2-4 sentences — grounding the goal in concrete user-visible terms. Name at least one specific current failure this fixes, so a reader arriving cold can tell what problem the rest of the ADR is solving. The Y-statement below formalizes the decision; this section lets a reader skim the headline without hunting through seven clauses first.}
247
+
205
248
  ## Y-Statement
206
249
 
207
250
  **In the context of:**
@@ -280,6 +323,7 @@ title: "{Title}"
280
323
  date: {YYYY-MM-DD}
281
324
  status: draft | approved | in-progress | completed | abandoned
282
325
  trajectory: required
326
+ rationale: required
283
327
  gate_policy: ask
284
328
  ---
285
329
 
@@ -319,6 +363,15 @@ For multi-phase impls, include a boundary map showing what each phase produces a
319
363
  - would require: {what would unlock a proper test — a new environment, a future plan, production data}
320
364
  - mitigation: {compensating control — telemetry alert, scheduled review, downstream plan, canary procedure, feedback signal}
321
365
 
366
+ ### Trajectory Rationale
367
+
368
+ **Starting assumption: every test is writable at Phase 0 (pre-plan) against the current stack — Phase 0 rows need no rationale.** This subsection is required ONLY when one or more rows have `Writable at` later than Phase 0. List one entry per Phase 1+ row, naming what prevents authoring the test before plan code lands. Read the entries together — if multiple rows share the same weak excuse, the plan is over-sequenced.
369
+
370
+ - **T3** `Writable at: Phase 2` — {one-sentence reason — typically because the subject under test is a TypeScript symbol authored in Phase 2 and the test file would not compile against today's stack}
371
+ - **T14** `Writable at: Phase 5` — {reason — e.g., "subject is the zod schema introduced in Phase 5; the test's import line is a compile error today"}
372
+
373
+ The `validate-impl-structure.js` hook enforces that every Phase 1+ T-ID from the trajectory table appears as an entry here. Phase 0 rows are exempt. Stale entries (rationale entries for IDs not in the trajectory) are flagged.
374
+
322
375
  ## Checklist
323
376
  ### Phase 1: {Name}
324
377
  - [ ] {Task — include code snippets when syntax matters}