@planu/cli 5.3.32 → 5.3.33
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,13 @@
|
|
|
1
|
+
## [5.3.33] - 2026-08-21
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(spec-1587): scope creation-time placeholder scan to standalone line content
|
|
5
|
+
- fix(spec-1586): harden scoreAmbiguity against tautological substring-pairs and unanchored markers
|
|
6
|
+
|
|
7
|
+
### Chores
|
|
8
|
+
- chore(planu): sync autopilot state for SPEC-1586/1587
|
|
9
|
+
|
|
10
|
+
|
|
1
11
|
## [5.3.32] - 2026-08-21
|
|
2
12
|
|
|
3
13
|
### Bug Fixes
|
|
@@ -144,6 +144,16 @@ function extractSection(body, sectionName) {
|
|
|
144
144
|
const end = next ? next.index : body.length;
|
|
145
145
|
return body.slice(start, end).trimEnd();
|
|
146
146
|
}
|
|
147
|
+
const CONTRACT_PLACEHOLDER_MARKERS = ['Needs decision', 'TBD', 'TODO'];
|
|
148
|
+
function stripLeadingListMarker(line) {
|
|
149
|
+
return line.replace(/^[ \t]*(?:[-*+]|\d+[.)])[ \t]+/, '');
|
|
150
|
+
}
|
|
151
|
+
function hasUnresolvedContractPlaceholderLine(scannable) {
|
|
152
|
+
return scannable.split('\n').some((line) => {
|
|
153
|
+
const stripped = stripLeadingListMarker(line).trim();
|
|
154
|
+
return (CONTRACT_PLACEHOLDER_MARKERS.some((marker) => marker.toLowerCase() === stripped.toLowerCase()) || /^(?:needs decision|tbd|todo)\s*:/i.test(stripped));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
147
157
|
/** Pure pre-persistence validation for the complete unified spec candidate. */
|
|
148
158
|
// eslint-disable-next-line max-lines-per-function -- validation order mirrors the public issue contract
|
|
149
159
|
export function validateUnifiedSpecCandidate(candidate, options = {}) {
|
|
@@ -188,7 +198,7 @@ export function validateUnifiedSpecCandidate(candidate, options = {}) {
|
|
|
188
198
|
message: `Required section heading is embedded in quoted, fenced, or nested content: ${section}`,
|
|
189
199
|
});
|
|
190
200
|
}
|
|
191
|
-
if (
|
|
201
|
+
if (hasUnresolvedContractPlaceholderLine(scannable)) {
|
|
192
202
|
issues.push({
|
|
193
203
|
code: 'UNRESOLVED_CONTRACT_PLACEHOLDER',
|
|
194
204
|
message: 'Unified candidate contains an unresolved contract placeholder.',
|
|
@@ -26,12 +26,12 @@ const VAGUE_WORDS = [
|
|
|
26
26
|
'intuitive',
|
|
27
27
|
];
|
|
28
28
|
const CONTRADICTION_PAIRS = [
|
|
29
|
-
['always', 'never'],
|
|
30
|
-
['mandatory', 'optional'],
|
|
31
|
-
['required', 'not required'],
|
|
32
|
-
['must', 'must not'],
|
|
33
|
-
['synchronous', 'asynchronous'],
|
|
34
|
-
['blocking', 'non-blocking'],
|
|
29
|
+
['always', 'never', 'genuine'],
|
|
30
|
+
['mandatory', 'optional', 'genuine'],
|
|
31
|
+
['required', 'not required', 'substring'],
|
|
32
|
+
['must', 'must not', 'substring'],
|
|
33
|
+
['synchronous', 'asynchronous', 'substring'],
|
|
34
|
+
['blocking', 'non-blocking', 'substring'],
|
|
35
35
|
];
|
|
36
36
|
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
37
37
|
function gradeFromTotal(total) {
|
|
@@ -234,6 +234,30 @@ function scoreTestability(criteriaLines) {
|
|
|
234
234
|
bddCoverage,
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
+
function wordBoundaryPattern(term) {
|
|
238
|
+
return new RegExp(`\\b${term}\\b`);
|
|
239
|
+
}
|
|
240
|
+
function hasWordBoundaryMatch(text, term) {
|
|
241
|
+
return wordBoundaryPattern(term).test(text);
|
|
242
|
+
}
|
|
243
|
+
function substringPairContradicts(line, shorter, longer) {
|
|
244
|
+
if (!hasWordBoundaryMatch(line, longer)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const withoutLonger = line.replace(new RegExp(`\\b${longer}\\b`, 'g'), ' ');
|
|
248
|
+
return hasWordBoundaryMatch(withoutLonger, shorter);
|
|
249
|
+
}
|
|
250
|
+
function genuinePairContradicts(line, termA, termB) {
|
|
251
|
+
return hasWordBoundaryMatch(line, termA) && hasWordBoundaryMatch(line, termB);
|
|
252
|
+
}
|
|
253
|
+
function pairContradictsInCriteria(criteriaLines, termA, termB, kind) {
|
|
254
|
+
return criteriaLines.some((rawLine) => {
|
|
255
|
+
const line = rawLine.toLowerCase();
|
|
256
|
+
return kind === 'substring'
|
|
257
|
+
? substringPairContradicts(line, termA, termB)
|
|
258
|
+
: genuinePairContradicts(line, termA, termB);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
237
261
|
function scoreAmbiguity(content, criteriaLines) {
|
|
238
262
|
const issues = [];
|
|
239
263
|
const recommendations = [];
|
|
@@ -247,10 +271,10 @@ function scoreAmbiguity(content, criteriaLines) {
|
|
|
247
271
|
};
|
|
248
272
|
}
|
|
249
273
|
const lower = content.toLowerCase();
|
|
250
|
-
// Check for contradiction pairs (-3 pts each, max -9)
|
|
274
|
+
// Check for contradiction pairs, scoped per criterion (-3 pts each, max -9)
|
|
251
275
|
let contradictions = 0;
|
|
252
|
-
for (const [termA, termB] of CONTRADICTION_PAIRS) {
|
|
253
|
-
if (
|
|
276
|
+
for (const [termA, termB, kind] of CONTRADICTION_PAIRS) {
|
|
277
|
+
if (pairContradictsInCriteria(criteriaLines, termA, termB, kind)) {
|
|
254
278
|
contradictions++;
|
|
255
279
|
if (contradictions <= 3) {
|
|
256
280
|
issues.push(`Potential contradiction: "${termA}" and "${termB}" both appear in spec`);
|
|
@@ -265,7 +289,7 @@ function scoreAmbiguity(content, criteriaLines) {
|
|
|
265
289
|
const undefinedTerms = ['tbd', 'todo', 'fixme', 'placeholder', 'to be defined', 'to be decided'];
|
|
266
290
|
let undefinedCount = 0;
|
|
267
291
|
for (const term of undefinedTerms) {
|
|
268
|
-
if (lower
|
|
292
|
+
if (hasWordBoundaryMatch(lower, term)) {
|
|
269
293
|
undefinedCount++;
|
|
270
294
|
issues.push(`Undefined placeholder found: "${term}"`);
|
|
271
295
|
}
|
package/package.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": "5.3.
|
|
5
|
+
"version": "5.3.33",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|