@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 (/\b(?:Needs decision|TBD|TODO)\b/i.test(scannable)) {
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 (lower.includes(termA) && lower.includes(termB)) {
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.includes(term)) {
292
+ if (hasWordBoundaryMatch(lower, term)) {
269
293
  undefinedCount++;
270
294
  issues.push(`Undefined placeholder found: "${term}"`);
271
295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.32",
3
+ "version": "5.3.33",
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",
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.32",
5
+ "version": "5.3.33",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",