@planu/cli 4.10.10 → 4.10.11

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,9 @@
1
+ ## [4.10.11] - 2026-07-08
2
+
3
+ ### Bug Fixes
4
+ - fix(validate): honor executable spec compliance score
5
+
6
+
1
7
  ## [4.10.10] - 2026-07-08
2
8
 
3
9
  ### Bug Fixes
@@ -2,8 +2,9 @@ import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
2
2
  import { runSpecCompliance, } from './spec-compliance-runner.js';
3
3
  export async function writeImplementationReviewReport(input) {
4
4
  const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
5
+ const reportScore = resolveExecutableValidationScore(input.score, specCompliance);
5
6
  const gates = buildGates({
6
- score: input.score,
7
+ score: reportScore,
7
8
  lintPassed: input.lintPassed,
8
9
  conventionRegression: input.conventionRegression ?? false,
9
10
  specCompliance,
@@ -25,7 +26,7 @@ export async function writeImplementationReviewReport(input) {
25
26
  reviewer,
26
27
  specCompliance: reportCompliance,
27
28
  minimalityReport: input.minimalityReport,
28
- score: input.score ?? undefined,
29
+ score: reportScore ?? undefined,
29
30
  completedAt: new Date().toISOString(),
30
31
  };
31
32
  try {
@@ -56,6 +57,9 @@ export async function writeImplementationReviewReport(input) {
56
57
  };
57
58
  }
58
59
  }
60
+ function resolveExecutableValidationScore(score, specCompliance) {
61
+ return specCompliance.perScenario.length > 0 ? specCompliance.dimensionScore : score;
62
+ }
59
63
  async function preserveUnobservedGates(input, nextGates) {
60
64
  const currentGateNames = new Set(nextGates.map((gate) => gate.name));
61
65
  const explicitlyObserved = new Set([
@@ -6,7 +6,7 @@ import { dirname, join } from 'node:path';
6
6
  import { formatLintFailureDiagnostics, resolveProjectCommandPlan, runProjectCommandPlan, } from './validate-runtime.js';
7
7
  /** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
8
8
  const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
9
- const DEFAULT_LINT_CHECK_TIMEOUT_MS = 10_000;
9
+ const DEFAULT_LINT_CHECK_TIMEOUT_MS = 60_000;
10
10
  const LINT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
11
11
  function isSafeCommand(cmd) {
12
12
  return SAFE_COMMAND_RE.test(cmd);
@@ -99,23 +99,24 @@ export async function handleValidate(args, server) {
99
99
  conventionRegression: regressionDetected,
100
100
  minimalityReport: minimalityReport ?? undefined,
101
101
  });
102
+ const effectiveResult = buildEffectiveValidationResult(result, validationReport.specCompliance);
102
103
  const qualityGateSummary = buildQualityGateSummary({
103
104
  conventionViolations,
104
105
  regressionDetected,
105
106
  lintPassed: lintCheck.passed,
106
107
  assurancePassed: assuranceGates.newCode.passed,
107
108
  minimalityBlocked: minimalityReport?.blocked === true,
108
- score: result.score,
109
+ score: effectiveResult.score,
109
110
  });
110
111
  const output = {
111
112
  specId,
112
113
  title: spec.title,
113
- score: result.score,
114
- fieldsImplemented: result.fieldsImplemented,
115
- fieldsTotal: result.fieldsTotal,
116
- matches: result.matches,
117
- missing: result.missing,
118
- extra: result.extra,
114
+ score: effectiveResult.score,
115
+ fieldsImplemented: effectiveResult.fieldsImplemented,
116
+ fieldsTotal: effectiveResult.fieldsTotal,
117
+ matches: effectiveResult.matches,
118
+ missing: effectiveResult.missing,
119
+ extra: effectiveResult.extra,
119
120
  qualityIssues: result.qualityIssues.map((issue) => ({
120
121
  file: issue.file,
121
122
  line: issue.line,
@@ -202,7 +203,7 @@ export async function handleValidate(args, server) {
202
203
  }
203
204
  // Auto-suggest corrective actions based on validation results
204
205
  // score is null when no criteria can be extracted — treat as 0 for comparisons.
205
- const scoreValue = result.score ?? 0;
206
+ const scoreValue = effectiveResult.score ?? 0;
206
207
  const suggestions = [];
207
208
  if (scoreValue < 50) {
208
209
  suggestions.push('Critical: Many acceptance criteria not implemented. Review spec requirements.');
@@ -216,8 +217,8 @@ export async function handleValidate(args, server) {
216
217
  if (!dod.isDone && spec.status === 'implementing') {
217
218
  suggestions.push('Implementation incomplete. Review DoD checklist before marking as done.');
218
219
  }
219
- if (result.missing.length > 0) {
220
- suggestions.push(`${result.missing.length} criteria still missing. Run detect_drift to identify gaps.`);
220
+ if (effectiveResult.missing.length > 0) {
221
+ suggestions.push(`${effectiveResult.missing.length} criteria still missing. Run detect_drift to identify gaps.`);
221
222
  }
222
223
  if (graphCoverage.gaps.length > 0) {
223
224
  suggestions.push(`${graphCoverage.gaps.length} graph-backed coverage gap(s) found without broad repository scan.`);
@@ -243,8 +244,8 @@ export async function handleValidate(args, server) {
243
244
  specId,
244
245
  input: { specType: spec.type, scope: spec.scope },
245
246
  outcome: {
246
- score: result.score,
247
- missingCount: result.missing.length,
247
+ score: effectiveResult.score,
248
+ missingCount: effectiveResult.missing.length,
248
249
  qualityIssueCount: result.qualityIssues.length,
249
250
  dorPassed: dor.isReady,
250
251
  dodPassed: dod.isDone,
@@ -261,7 +262,7 @@ export async function handleValidate(args, server) {
261
262
  type: 'validate',
262
263
  specId,
263
264
  input: { trigger: 'auto-quality-learn', rules: topIssueRules },
264
- outcome: { score: result.score, antipatterns: topIssueRules },
265
+ outcome: { score: effectiveResult.score, antipatterns: topIssueRules },
265
266
  delta: { qualityGap: 100 - scoreValue },
266
267
  timestamp: new Date().toISOString(),
267
268
  }).catch((err) => {
@@ -277,10 +278,10 @@ export async function handleValidate(args, server) {
277
278
  ? '\n\nšŸ“ HTML reports updated — commit to save: `git add planu/specs/ planu/conventions.json && git commit -m "chore(planu): update reports"`'
278
279
  : '';
279
280
  // SPEC-512: Compact output — only surface failures, cap to 200 tokens
280
- const totalCriteria = result.fieldsTotal;
281
- const passedCount = result.fieldsImplemented;
281
+ const totalCriteria = effectiveResult.fieldsTotal;
282
+ const passedCount = effectiveResult.fieldsImplemented;
282
283
  const failedCount = totalCriteria - passedCount;
283
- const indeterminate = result.indeterminate ?? [];
284
+ const indeterminate = effectiveResult.indeterminate ?? [];
284
285
  const effectiveScore = qualityGateSummary.effectiveScore ?? scoreValue;
285
286
  const compactText = buildCompactValidateText({
286
287
  score: effectiveScore,
@@ -288,20 +289,20 @@ export async function handleValidate(args, server) {
288
289
  failedCount,
289
290
  totalCriteria,
290
291
  qualityFailures: qualityGateSummary.failures,
291
- missing: result.missing,
292
+ missing: effectiveResult.missing,
292
293
  indeterminate,
293
294
  commitReminder,
294
295
  planuReminder,
295
296
  });
296
297
  const graphText = formatGraphCoverageText(graphCoverage);
297
298
  // SPEC-512: Compact structuredContent — essential fields only at top level
298
- const compactSummary = buildCompactSummary(specId, result.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
299
+ const compactSummary = buildCompactSummary(specId, effectiveResult.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
299
300
  const structuredBase = buildValidateStructuredContent({
300
301
  specId,
301
302
  title: spec.title,
302
- score: result.score === null ? null : effectiveScore,
303
+ score: effectiveResult.score === null ? null : effectiveScore,
303
304
  compactSummary,
304
- result,
305
+ result: effectiveResult,
305
306
  indeterminate,
306
307
  dor,
307
308
  dod,
@@ -384,7 +385,6 @@ function buildQualityGateSummary(input) {
384
385
  const blockingConventionViolations = input.conventionViolations.filter((violation) => violation.severity === 'critical');
385
386
  const failures = [
386
387
  ...(input.regressionDetected ? ['convention-regression'] : []),
387
- ...(blockingConventionViolations.length > 0 ? ['blocking-convention-violations'] : []),
388
388
  ...(!input.lintPassed ? ['lint'] : []),
389
389
  ...(!input.assurancePassed ? ['assurance'] : []),
390
390
  ...(input.minimalityBlocked ? ['minimality'] : []),
@@ -400,6 +400,40 @@ function buildQualityGateSummary(input) {
400
400
  minimalityPassed: !input.minimalityBlocked,
401
401
  };
402
402
  }
403
+ function buildExecutableCoverage(specCompliance) {
404
+ if (specCompliance.scenarios.length === 0) {
405
+ return null;
406
+ }
407
+ const matches = specCompliance.scenarios
408
+ .filter((scenario) => scenario.verdict === 'pass')
409
+ .map((scenario) => scenario.title);
410
+ const missing = specCompliance.scenarios
411
+ .filter((scenario) => scenario.verdict !== 'pass')
412
+ .map((scenario) => scenario.title);
413
+ return {
414
+ score: specCompliance.score,
415
+ fieldsImplemented: matches.length,
416
+ fieldsTotal: specCompliance.scenarios.length,
417
+ matches,
418
+ missing,
419
+ indeterminate: [],
420
+ };
421
+ }
422
+ function buildEffectiveValidationResult(result, specCompliance) {
423
+ const executableCoverage = buildExecutableCoverage(specCompliance);
424
+ if (executableCoverage === null) {
425
+ return result;
426
+ }
427
+ return {
428
+ ...result,
429
+ score: executableCoverage.score,
430
+ fieldsImplemented: executableCoverage.fieldsImplemented,
431
+ fieldsTotal: executableCoverage.fieldsTotal,
432
+ matches: executableCoverage.matches,
433
+ missing: executableCoverage.missing,
434
+ indeterminate: executableCoverage.indeterminate,
435
+ };
436
+ }
403
437
  function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria) {
404
438
  return {
405
439
  specId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.10.10",
3
+ "version": "4.10.11",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.10.10",
38
- "@planu/core-darwin-x64": "4.10.10",
39
- "@planu/core-linux-arm64-gnu": "4.10.10",
40
- "@planu/core-linux-arm64-musl": "4.10.10",
41
- "@planu/core-linux-x64-gnu": "4.10.10",
42
- "@planu/core-linux-x64-musl": "4.10.10",
43
- "@planu/core-win32-arm64-msvc": "4.10.10",
44
- "@planu/core-win32-x64-msvc": "4.10.10"
37
+ "@planu/core-darwin-arm64": "4.10.11",
38
+ "@planu/core-darwin-x64": "4.10.11",
39
+ "@planu/core-linux-arm64-gnu": "4.10.11",
40
+ "@planu/core-linux-arm64-musl": "4.10.11",
41
+ "@planu/core-linux-x64-gnu": "4.10.11",
42
+ "@planu/core-linux-x64-musl": "4.10.11",
43
+ "@planu/core-win32-arm64-msvc": "4.10.11",
44
+ "@planu/core-win32-x64-msvc": "4.10.11"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.10.10",
4
+ "version": "4.10.11",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
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": "4.10.10",
5
+ "version": "4.10.11",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",