@planu/cli 5.3.58 → 5.3.60

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 CHANGED
@@ -1,3 +1,29 @@
1
+ ## [5.3.60] - 2026-08-26
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1634): normalize scenario test-link descriptions across colon spacing
5
+ - fix(SPEC-1634): accept path-shaped colon forms in scenario tests entries
6
+ - fix(SPEC-1633): point the documented human validation gate at validate:full
7
+
8
+ ### Refactoring
9
+ - refactor(SPEC-1633): dedupe validate against check:strict in the release plan
10
+
11
+ ### Chores
12
+ - chore(planu): close SPEC-1633 and SPEC-1634
13
+ - chore(SPEC-1633,SPEC-1634): normalize file ownership and risk sections for the handoff gate
14
+ - chore(SPEC-1633,SPEC-1634): approve both specs with challenge resolution
15
+
16
+
17
+ ## [5.3.59] - 2026-08-26
18
+
19
+ ### Bug Fixes
20
+ - fix(SPEC-1631): report graph coverage as unavailable instead of zero gaps
21
+
22
+ ### Chores
23
+ - chore(planu): close SPEC-1631
24
+ - chore(planu): record SPEC-1631 implementing transition
25
+
26
+
1
27
  ## [5.3.58] - 2026-08-26
2
28
 
3
29
  ### Bug Fixes
package/README.md CHANGED
@@ -114,9 +114,9 @@ planu/ # Portable, version-controlled project contract
114
114
 
115
115
  ```bash
116
116
  pnpm install # Install dependencies
117
- pnpm build # Compile TS + Build Rust bridge
117
+ pnpm build # Compile TypeScript
118
118
  pnpm dev # Watch mode
119
- pnpm validate # Canonical local validation contract
119
+ pnpm validate:full # Build plus every static gate plus the full suite
120
120
  pnpm check:strict # Extended local quality gates
121
121
  pnpm test # Run the full test suite
122
122
  pnpm release:local # Local-first release flow when shipping
@@ -1 +1 @@
1
- {"schemaVersion":1,"commit":"27eba0edeec0ce821f730eebbbbd999f5a1725b6"}
1
+ {"schemaVersion":1,"commit":"178163df95059021ac364f72b65da84d4a145c66"}
@@ -5,6 +5,7 @@ export interface ScenarioTestLink {
5
5
  line?: number;
6
6
  runner?: string;
7
7
  config?: string;
8
+ description?: string;
8
9
  }
9
10
  export interface ScenarioStep {
10
11
  keyword: string;
@@ -17,6 +17,35 @@ export class ComplianceCommandTerminalError extends Error {
17
17
  this.name = 'ComplianceCommandTerminalError';
18
18
  }
19
19
  }
20
+ const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
21
+ const PATH_SHAPED = /[/\\]/;
22
+ const FILE_EXTENSION = /\.[A-Za-z0-9]+$/;
23
+ function isPathShapedSegment(segment) {
24
+ return segment.length > 0 && (PATH_SHAPED.test(segment) || FILE_EXTENSION.test(segment));
25
+ }
26
+ function parseTestLinkString(raw) {
27
+ if (WINDOWS_DRIVE_PATH.test(raw)) {
28
+ return { path: raw };
29
+ }
30
+ const colonIndex = raw.indexOf(':');
31
+ if (colonIndex === -1) {
32
+ return { path: raw };
33
+ }
34
+ const path = raw.slice(0, colonIndex);
35
+ if (!isPathShapedSegment(path)) {
36
+ return null;
37
+ }
38
+ const remainder = raw.slice(colonIndex + 1);
39
+ const remainderColonIndex = remainder.indexOf(':');
40
+ const linePart = remainderColonIndex === -1 ? remainder : remainder.slice(0, remainderColonIndex);
41
+ if (/^\d+$/.test(linePart)) {
42
+ const description = remainderColonIndex === -1 ? undefined : remainder.slice(remainderColonIndex + 1).trim();
43
+ const line = Number.parseInt(linePart, 10);
44
+ return description ? { path, line, description } : { path, line };
45
+ }
46
+ const description = remainder.trim();
47
+ return description ? { path, description } : { path };
48
+ }
20
49
  // eslint-disable-next-line complexity, max-lines-per-function -- parses the supported scenario YAML subset without executing it
21
50
  export function parseFrontmatterScenarios(raw) {
22
51
  const match = /^---\n([\s\S]*?)\n---\n/.exec(raw);
@@ -105,9 +134,10 @@ export function parseFrontmatterScenarios(raw) {
105
134
  continue;
106
135
  }
107
136
  const stringMatch = /^\s+-\s+["']?([^'":\s][^'"]*?)["']?\s*$/.exec(trimmed);
108
- if (stringMatch?.[1] && !stringMatch[1].includes(':')) {
137
+ const testLink = stringMatch?.[1] ? parseTestLinkString(stringMatch[1]) : null;
138
+ if (testLink) {
109
139
  currentScenario.tests ??= [];
110
- currentScenario.tests.push({ path: stringMatch[1] });
140
+ currentScenario.tests.push(testLink);
111
141
  continue;
112
142
  }
113
143
  const metadataMatch = /^\s+(line|runner|config):\s*["']?(.+?)["']?\s*$/.exec(trimmed);
@@ -19,6 +19,10 @@ export declare const ValidateCompletionOutputSchema: {
19
19
  failing: "failing";
20
20
  passing: "passing";
21
21
  }>;
22
+ graphAvailability: z.ZodEnum<{
23
+ available: "available";
24
+ unavailable: "unavailable";
25
+ }>;
22
26
  }, z.core.$strip>;
23
27
  counts: z.ZodObject<{
24
28
  matches: z.ZodNumber;
@@ -198,6 +202,10 @@ export declare const ValidateCompletionOutputSchema: {
198
202
  }, z.core.$strip>>;
199
203
  }, z.core.$strip>>;
200
204
  graphCoverage: z.ZodObject<{
205
+ availability: z.ZodEnum<{
206
+ available: "available";
207
+ unavailable: "unavailable";
208
+ }>;
201
209
  gapsCount: z.ZodNumber;
202
210
  gapsTop5: z.ZodArray<z.ZodString>;
203
211
  freshness: z.ZodOptional<z.ZodObject<{
@@ -40,6 +40,7 @@ export const ValidateCompletionOutputSchema = {
40
40
  failed: z.number(),
41
41
  total: z.number(),
42
42
  status: z.enum(['passing', 'failing']),
43
+ graphAvailability: z.enum(['available', 'unavailable']),
43
44
  }),
44
45
  counts: z.object({
45
46
  matches: z.number(),
@@ -162,6 +163,7 @@ export const ValidateCompletionOutputSchema = {
162
163
  })
163
164
  .optional(),
164
165
  graphCoverage: z.object({
166
+ availability: z.enum(['available', 'unavailable']),
165
167
  gapsCount: z.number(),
166
168
  gapsTop5: z.array(z.string()),
167
169
  freshness: z
@@ -1,9 +1,7 @@
1
1
  // tools/validate-graph-coverage.ts — Graph coverage helpers for validate.
2
+ import { reportClassifiedDegradation } from '../errors/classified-degradation.js';
2
3
  import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
3
4
  function graphCoverageGaps(slice) {
4
- if (slice === null) {
5
- return [];
6
- }
7
5
  const criteria = slice.nodes.filter((node) => node.type === 'criterion');
8
6
  return criteria
9
7
  .filter((criterion) => {
@@ -14,15 +12,29 @@ function graphCoverageGaps(slice) {
14
12
  .slice(0, 10);
15
13
  }
16
14
  export async function buildGraphCoverageReport(args) {
17
- const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
15
+ let graphSlice;
16
+ try {
17
+ graphSlice = await queryProjectGraphSlice(args);
18
+ }
19
+ catch (error) {
20
+ reportClassifiedDegradation('GRAPH_COVERAGE_UNAVAILABLE', error);
21
+ graphSlice = null;
22
+ }
23
+ if (graphSlice === null) {
24
+ return { availability: 'unavailable', gaps: [], compactNodes: 0, compactEdges: 0 };
25
+ }
18
26
  return {
19
- freshness: graphSlice?.freshness,
27
+ availability: 'available',
28
+ freshness: graphSlice.freshness,
20
29
  gaps: graphCoverageGaps(graphSlice),
21
- compactNodes: graphSlice?.nodes.length ?? 0,
22
- compactEdges: graphSlice?.edges.length ?? 0,
30
+ compactNodes: graphSlice.nodes.length,
31
+ compactEdges: graphSlice.edges.length,
23
32
  };
24
33
  }
25
34
  export function formatGraphCoverageText(report) {
35
+ if (report.availability === 'unavailable') {
36
+ return '\nGRAPH unavailable (project graph unreadable)';
37
+ }
26
38
  return report.gaps.length > 0
27
39
  ? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
28
40
  : '';
@@ -583,7 +583,7 @@ export async function executeValidate(args, server, onProgress) {
583
583
  });
584
584
  const graphText = formatGraphCoverageText(graphCoverage);
585
585
  // SPEC-512: Compact structuredContent — essential fields only at top level
586
- const compactSummary = buildCompactSummary(specId, effectiveResult.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
586
+ const compactSummary = buildCompactSummary(specId, effectiveResult.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria, graphCoverage.availability);
587
587
  const structuredBase = buildValidateStructuredContent({
588
588
  specId,
589
589
  title: spec.title,
@@ -718,7 +718,7 @@ function buildEffectiveValidationResult(result, specCompliance) {
718
718
  indeterminate: executableCoverage.indeterminate,
719
719
  };
720
720
  }
721
- function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria) {
721
+ function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria, graphAvailability) {
722
722
  return {
723
723
  specId,
724
724
  score,
@@ -726,6 +726,7 @@ function buildCompactSummary(specId, score, passedCount, failedCount, totalCrite
726
726
  failed: failedCount,
727
727
  total: totalCriteria,
728
728
  status: failedCount === 0 ? 'passing' : 'failing',
729
+ graphAvailability,
729
730
  };
730
731
  }
731
732
  function buildValidateStructuredContent(args) {
@@ -859,10 +860,10 @@ function buildValidateStructuredContent(args) {
859
860
  }
860
861
  : undefined,
861
862
  graphCoverage: {
863
+ availability: args.graphCoverage.availability,
862
864
  gapsCount: args.graphCoverage.gaps.length,
863
865
  gapsTop5: args.graphCoverage.gaps.slice(0, 5),
864
866
  freshness: args.graphCoverage.freshness,
865
- summary: args.graphCoverage.summary,
866
867
  },
867
868
  suggestionsTop5: args.suggestions.slice(0, 5),
868
869
  });
@@ -198,6 +198,7 @@ export interface ProjectGraphQueryInput {
198
198
  maxEdges?: number;
199
199
  }
200
200
  export interface GraphCoverageReport {
201
+ availability: 'available' | 'unavailable';
201
202
  freshness?: ProjectGraphFreshness;
202
203
  gaps: string[];
203
204
  compactNodes: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.58",
3
+ "version": "5.3.60",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "generate:host-tool-registry": "node scripts/generate-host-tool-registry.mjs",
30
30
  "release:local": "bash scripts/release-local.sh",
31
31
  "release:publish": "bash scripts/release-local.sh --publish",
32
- "build": "pnpm clean && pnpm build:ts",
32
+ "build": "pnpm build:ts",
33
33
  "build:ts": "pnpm clean && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node scripts/copy-runtime-assets.mjs",
34
34
  "build:obfuscated": "pnpm build && node scripts/obfuscate.mjs",
35
35
  "dev": "tsc --watch",
@@ -86,7 +86,8 @@
86
86
  "prepublishOnly": "bash scripts/prepublish-guard.sh",
87
87
  "prepack": "pnpm build:ts && pnpm package:size",
88
88
  "test:debug": "vitest run --inspect-brk --single-thread",
89
- "validate": "pnpm build && pnpm typecheck && pnpm verify:typescript-migration && pnpm lint:gate && pnpm format:check && pnpm test:release-gate",
89
+ "validate": "pnpm build && pnpm verify:typescript-migration",
90
+ "validate:full": "pnpm build && pnpm typecheck && pnpm verify:typescript-migration && pnpm lint:gate && pnpm format:check && pnpm test:coverage",
90
91
  "docker:build": "docker build -t planu .",
91
92
  "docker:run": "docker compose up",
92
93
  "publish:blog": "node scripts/publish-blog.mjs",
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "5.3.58",
5
+ "version": "5.3.60",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",