@planu/cli 4.11.3 → 4.11.4
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 +7 -0
- package/dist/engine/implementation-contract/evaluator.js +4 -1
- package/dist/tools/challenge-spec/challenge-report.d.ts +3 -0
- package/dist/tools/challenge-spec/challenge-report.js +71 -6
- package/dist/tools/challenge-spec.js +5 -1
- package/dist/tools/update-status/transition-guard.d.ts +1 -1
- package/dist/tools/update-status/transition-guard.js +9 -5
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,8 @@ const FILLER_PATTERNS = [
|
|
|
6
6
|
/\bhandle errors\b/i,
|
|
7
7
|
/\bto be determined\b/i,
|
|
8
8
|
];
|
|
9
|
+
const MANUAL_VERIFICATION_STEP_RE = /\b(given|when|then|step|observe|click|run)\b/i;
|
|
10
|
+
const CONCRETE_MANUAL_EVIDENCE_RE = /`[^`]+`|\b(?:src|tests|docs|scripts|website|planu)\/[\w./-]+/i;
|
|
9
11
|
export function evaluateImplementationContract(specBody, acceptanceCriteria) {
|
|
10
12
|
const contract = extractTopLevelSection(specBody, IMPLEMENTATION_CONTRACT_SECTION);
|
|
11
13
|
if (contract.trim().length === 0) {
|
|
@@ -62,7 +64,8 @@ export function evaluateImplementationContract(specBody, acceptanceCriteria) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
if (/\bmanual verification\b/i.test(map) &&
|
|
65
|
-
|
|
67
|
+
!MANUAL_VERIFICATION_STEP_RE.test(map) &&
|
|
68
|
+
!CONCRETE_MANUAL_EVIDENCE_RE.test(map)) {
|
|
66
69
|
issues.push({
|
|
67
70
|
code: 'contract_manual_verification_vague',
|
|
68
71
|
message: 'Manual verification in the Implementation Contract must include exact observable steps.',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ChallengeReport, ChallengeResolutionEvidence, FailureScenario } from '../../types/index.js';
|
|
2
2
|
export declare const MINIMUM_RESOLVED_CHALLENGES = 3;
|
|
3
|
+
export declare function requiredResolvedChallenges(totalScenarios: number): number;
|
|
4
|
+
export declare function parseChallengeResolutionEvidence(specContent: string, scenarios: readonly FailureScenario[], runAt: string): ChallengeResolutionEvidence[];
|
|
3
5
|
/** Count only evidence tied to findings persisted by the same challenge report. */
|
|
4
6
|
export declare function getResolvedChallengeEvidence(report: ChallengeReport | undefined): ChallengeResolutionEvidence[];
|
|
5
7
|
/** Build a report whose resolution state is derived exclusively from explicit evidence. */
|
|
@@ -8,6 +10,7 @@ export declare function buildChallengeReport(input: {
|
|
|
8
10
|
focusAreas: string[];
|
|
9
11
|
overallRisk: ChallengeReport['overallRisk'];
|
|
10
12
|
previousReport?: ChallengeReport;
|
|
13
|
+
explicitResolutionEvidence?: ChallengeResolutionEvidence[];
|
|
11
14
|
runAt?: string;
|
|
12
15
|
}): ChallengeReport;
|
|
13
16
|
//# sourceMappingURL=challenge-report.d.ts.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const MINIMUM_RESOLVED_CHALLENGES = 3;
|
|
2
|
+
export function requiredResolvedChallenges(totalScenarios) {
|
|
3
|
+
return Math.min(MINIMUM_RESOLVED_CHALLENGES, Math.max(0, totalScenarios));
|
|
4
|
+
}
|
|
2
5
|
function isExplicitResolutionEvidence(value) {
|
|
3
6
|
if (typeof value !== 'object' || value === null) {
|
|
4
7
|
return false;
|
|
@@ -12,16 +15,72 @@ function isExplicitResolutionEvidence(value) {
|
|
|
12
15
|
typeof candidate.resolvedAt === 'string' &&
|
|
13
16
|
candidate.resolvedAt.trim().length > 0);
|
|
14
17
|
}
|
|
15
|
-
function uniqueMatchingEvidence(
|
|
16
|
-
const evidence = Array.isArray(report?.resolutionEvidence) ? report.resolutionEvidence : [];
|
|
18
|
+
function uniqueMatchingEvidence(evidence, scenarioNames) {
|
|
17
19
|
const matched = new Map();
|
|
18
|
-
for (const item of evidence) {
|
|
20
|
+
for (const item of evidence ?? []) {
|
|
19
21
|
if (isExplicitResolutionEvidence(item) && scenarioNames.has(item.scenario)) {
|
|
20
22
|
matched.set(item.scenario, item);
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
return [...matched.values()];
|
|
24
26
|
}
|
|
27
|
+
function inferResolution(text) {
|
|
28
|
+
return /\b(accepted|accept risk|accepted risk|known risk)\b/i.test(text)
|
|
29
|
+
? 'accepted'
|
|
30
|
+
: 'mitigated';
|
|
31
|
+
}
|
|
32
|
+
function extractChallengeResolutionSection(specContent) {
|
|
33
|
+
const normalized = specContent.replace(/\r\n/g, '\n');
|
|
34
|
+
const headingRe = /^###\s+Challenge Resolution[ \t]*$/m;
|
|
35
|
+
const match = headingRe.exec(normalized);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
const start = match.index + match[0].length;
|
|
40
|
+
const nextRe = /^###\s+\S/gm;
|
|
41
|
+
nextRe.lastIndex = start;
|
|
42
|
+
const next = nextRe.exec(normalized);
|
|
43
|
+
return normalized.slice(start, next ? next.index : normalized.length).trim();
|
|
44
|
+
}
|
|
45
|
+
export function parseChallengeResolutionEvidence(specContent, scenarios, runAt) {
|
|
46
|
+
const section = extractChallengeResolutionSection(specContent);
|
|
47
|
+
if (section.length === 0 || scenarios.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (scenarios.length === 1) {
|
|
51
|
+
const [singleScenario] = scenarios;
|
|
52
|
+
if (!singleScenario) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
scenario: singleScenario.scenario,
|
|
58
|
+
resolution: inferResolution(section),
|
|
59
|
+
evidence: section.replace(/\s+/g, ' ').trim(),
|
|
60
|
+
resolvedAt: runAt,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
const bullets = section
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => /^-\s+/.test(line))
|
|
68
|
+
.map((line) => line.replace(/^-\s+/, '').trim());
|
|
69
|
+
const evidence = [];
|
|
70
|
+
for (const bullet of bullets) {
|
|
71
|
+
const scenario = scenarios.find((item) => bullet.includes(item.scenario));
|
|
72
|
+
if (!scenario) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
evidence.push({
|
|
76
|
+
scenario: scenario.scenario,
|
|
77
|
+
resolution: inferResolution(bullet),
|
|
78
|
+
evidence: bullet,
|
|
79
|
+
resolvedAt: runAt,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return evidence;
|
|
83
|
+
}
|
|
25
84
|
/** Count only evidence tied to findings persisted by the same challenge report. */
|
|
26
85
|
export function getResolvedChallengeEvidence(report) {
|
|
27
86
|
if (!Array.isArray(report?.findings)) {
|
|
@@ -30,21 +89,27 @@ export function getResolvedChallengeEvidence(report) {
|
|
|
30
89
|
const scenarioNames = new Set(report.findings
|
|
31
90
|
.map((finding) => finding.scenario)
|
|
32
91
|
.filter((scenario) => typeof scenario === 'string' && scenario.trim().length > 0));
|
|
33
|
-
return uniqueMatchingEvidence(report, scenarioNames);
|
|
92
|
+
return uniqueMatchingEvidence(report.resolutionEvidence, scenarioNames);
|
|
34
93
|
}
|
|
35
94
|
/** Build a report whose resolution state is derived exclusively from explicit evidence. */
|
|
36
95
|
export function buildChallengeReport(input) {
|
|
37
96
|
const scenarioNames = new Set(input.scenarios.map((scenario) => scenario.scenario));
|
|
38
|
-
const resolutionEvidence = uniqueMatchingEvidence(
|
|
97
|
+
const resolutionEvidence = uniqueMatchingEvidence([
|
|
98
|
+
...(Array.isArray(input.previousReport?.resolutionEvidence)
|
|
99
|
+
? input.previousReport.resolutionEvidence
|
|
100
|
+
: []),
|
|
101
|
+
...(Array.isArray(input.explicitResolutionEvidence) ? input.explicitResolutionEvidence : []),
|
|
102
|
+
], scenarioNames);
|
|
39
103
|
const resolvedScenarios = new Set(resolutionEvidence.map((item) => item.scenario));
|
|
40
104
|
const addressedCount = resolvedScenarios.size;
|
|
105
|
+
const requiredCount = requiredResolvedChallenges(input.scenarios.length);
|
|
41
106
|
return {
|
|
42
107
|
runAt: input.runAt ?? new Date().toISOString(),
|
|
43
108
|
totalScenarios: input.scenarios.length,
|
|
44
109
|
addressedCount,
|
|
45
110
|
focusAreas: input.focusAreas,
|
|
46
111
|
overallRisk: input.overallRisk,
|
|
47
|
-
passed: addressedCount >=
|
|
112
|
+
passed: addressedCount >= requiredCount,
|
|
48
113
|
findings: input.scenarios.map((scenario) => ({
|
|
49
114
|
scenario: scenario.scenario,
|
|
50
115
|
resolved: resolvedScenarios.has(scenario.scenario),
|
|
@@ -18,7 +18,7 @@ import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-bu
|
|
|
18
18
|
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
19
19
|
import { detectChallengeCapabilities, } from './challenge-spec/scenarios-utils.js';
|
|
20
20
|
import { collectCapabilityScenarios } from './challenge-spec/scenario-collector.js';
|
|
21
|
-
import { buildChallengeReport } from './challenge-spec/challenge-report.js';
|
|
21
|
+
import { buildChallengeReport, parseChallengeResolutionEvidence, } from './challenge-spec/challenge-report.js';
|
|
22
22
|
const ALL_FOCUS_AREAS = [
|
|
23
23
|
'failures',
|
|
24
24
|
'concurrency',
|
|
@@ -194,11 +194,15 @@ export async function handleChallengeSpec(args, server) {
|
|
|
194
194
|
scalabilityAssessment,
|
|
195
195
|
overallRisk,
|
|
196
196
|
};
|
|
197
|
+
const challengeRunAt = new Date().toISOString();
|
|
198
|
+
const explicitResolutionEvidence = parseChallengeResolutionEvidence(specContent, failureScenariosScored, challengeRunAt);
|
|
197
199
|
const challengeReport = buildChallengeReport({
|
|
198
200
|
scenarios: failureScenariosScored,
|
|
199
201
|
focusAreas,
|
|
200
202
|
overallRisk,
|
|
201
203
|
previousReport: spec.challengeReport,
|
|
204
|
+
explicitResolutionEvidence,
|
|
205
|
+
runAt: challengeRunAt,
|
|
202
206
|
});
|
|
203
207
|
const rawHumanSummary = buildChallengeSpecSummary(prioritized, overallRisk);
|
|
204
208
|
// SPEC-620: Inject token budget tag into the LLM-facing summary prompt
|
|
@@ -76,7 +76,7 @@ export declare function checkReadinessGate(spec: Spec, newStatus: SpecStatus, fo
|
|
|
76
76
|
* or does not contain enough explicit resolution evidence.
|
|
77
77
|
*
|
|
78
78
|
* - Requires challengeReport to exist on the spec
|
|
79
|
-
* - Requires
|
|
79
|
+
* - Requires explicit mitigation/risk-acceptance evidence for up to the first 3 findings
|
|
80
80
|
* - High-risk specs (high/critical) require all 5 focus areas
|
|
81
81
|
*/
|
|
82
82
|
export declare function checkChallengeGate(spec: Spec, newStatus: SpecStatus): ToolResult | null;
|
|
@@ -9,7 +9,7 @@ import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-
|
|
|
9
9
|
import { checkGroundedSpecContract } from '../../engine/spec-grounding/contract.js';
|
|
10
10
|
import { checkGenericSpecOutput } from '../../engine/spec-quality/generic-output-gate.js';
|
|
11
11
|
import { formatKeyValue } from '../output-formatter.js';
|
|
12
|
-
import { getResolvedChallengeEvidence,
|
|
12
|
+
import { getResolvedChallengeEvidence, requiredResolvedChallenges, } from '../challenge-spec/challenge-report.js';
|
|
13
13
|
/**
|
|
14
14
|
* Valid state transitions for spec lifecycle.
|
|
15
15
|
* draft -> review -> approved -> implementing -> done
|
|
@@ -436,7 +436,7 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
|
436
436
|
* or does not contain enough explicit resolution evidence.
|
|
437
437
|
*
|
|
438
438
|
* - Requires challengeReport to exist on the spec
|
|
439
|
-
* - Requires
|
|
439
|
+
* - Requires explicit mitigation/risk-acceptance evidence for up to the first 3 findings
|
|
440
440
|
* - High-risk specs (high/critical) require all 5 focus areas
|
|
441
441
|
*/
|
|
442
442
|
export function checkChallengeGate(spec, newStatus) {
|
|
@@ -462,12 +462,16 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
462
462
|
}
|
|
463
463
|
const resolutionEvidence = getResolvedChallengeEvidence(report);
|
|
464
464
|
const addressedCount = resolutionEvidence.length;
|
|
465
|
-
|
|
465
|
+
const findingsCount = Array.isArray(report.findings)
|
|
466
|
+
? report.findings.length
|
|
467
|
+
: report.totalScenarios;
|
|
468
|
+
const requiredCount = requiredResolvedChallenges(findingsCount);
|
|
469
|
+
if (addressedCount < requiredCount) {
|
|
466
470
|
return {
|
|
467
471
|
content: [
|
|
468
472
|
{
|
|
469
473
|
type: 'text',
|
|
470
|
-
text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(
|
|
474
|
+
text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record mitigation or accepted-risk evidence for discovered findings before retrying.`,
|
|
471
475
|
},
|
|
472
476
|
],
|
|
473
477
|
isError: true,
|
|
@@ -475,7 +479,7 @@ export function checkChallengeGate(spec, newStatus) {
|
|
|
475
479
|
error: 'CHALLENGE_GATE_BLOCKED',
|
|
476
480
|
code: 'INSUFFICIENT_CHALLENGES',
|
|
477
481
|
addressedCount,
|
|
478
|
-
requiredCount
|
|
482
|
+
requiredCount,
|
|
479
483
|
fixHint: `Resolve challenge_spec findings with concrete evidence, then retry update_status(specId="${spec.id}", status="review").`,
|
|
480
484
|
},
|
|
481
485
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.11.
|
|
3
|
+
"version": "4.11.4",
|
|
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.11.
|
|
38
|
-
"@planu/core-darwin-x64": "4.11.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.11.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.11.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.11.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.11.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.11.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.11.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.11.4",
|
|
38
|
+
"@planu/core-darwin-x64": "4.11.4",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.11.4",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.11.4",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.11.4",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.11.4",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.11.4",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.11.4"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
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.11.
|
|
5
|
+
"version": "4.11.4",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|