@planu/cli 5.3.31 → 5.3.32
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
|
@@ -37,12 +37,35 @@ const STOP_WORDS = new Set([
|
|
|
37
37
|
'all',
|
|
38
38
|
'can',
|
|
39
39
|
'may',
|
|
40
|
+
'rather',
|
|
41
|
+
'than',
|
|
42
|
+
'which',
|
|
43
|
+
'would',
|
|
44
|
+
'such',
|
|
45
|
+
'same',
|
|
46
|
+
'each',
|
|
47
|
+
'also',
|
|
48
|
+
'over',
|
|
49
|
+
'onto',
|
|
40
50
|
]);
|
|
41
51
|
function keywords(phrase) {
|
|
42
52
|
return normalize(phrase)
|
|
43
53
|
.split(' ')
|
|
44
54
|
.filter((w) => w.length > 3 && !STOP_WORDS.has(w));
|
|
45
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Shared negation detector: "not/without ..." or "no <non-idiom> ..." immediately before a verb.
|
|
58
|
+
* Excludes adverbial idioms ("no doubt/longer/way/matter") that do NOT negate the verb.
|
|
59
|
+
*/
|
|
60
|
+
function endsWithNegation(before) {
|
|
61
|
+
if (/\b(?:not|without|never)(?:\s+\w+){0,6}\s*$/.test(before)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!/\bno(?:\s+\w+){0,6}\s*$/.test(before)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return !/\bno\s+(?:doubt)\b(?:\s+\w+){0,5}\s*$/.test(before);
|
|
68
|
+
}
|
|
46
69
|
/** Return true only for explicit language that preserves an existing boundary. */
|
|
47
70
|
function preservesBoundary(text) {
|
|
48
71
|
const normalized = normalize(text);
|
|
@@ -53,8 +76,19 @@ function preservesBoundary(text) {
|
|
|
53
76
|
/\bkeeps?\b.{0,80}\bunchanged\b/,
|
|
54
77
|
/\bpreserves?\b.{0,80}\b(?:behavior|contract|boundary|semantics|state)\b/,
|
|
55
78
|
/\bwithout (?:any )?(?:changing|modifying|altering|adding|introducing)\b/,
|
|
56
|
-
/\b(?:does|do|must|will|should) not (?:change|modify|alter|add|introduce|implement|enable)\b/,
|
|
79
|
+
/\b(?:does|do|must|will|should) not (?:change|modify|alter|add|introduce|implement|enable|upgrade)\b/,
|
|
57
80
|
/\bno changes? to\b/,
|
|
81
|
+
/\bno\b(?!\s+(?:doubt)\b)(?:\s+\w+){0,6}\s+changes?\b/,
|
|
82
|
+
/\bunchanged\b/,
|
|
83
|
+
/\bstill contains\b.{0,60}\bunchanged\b/,
|
|
84
|
+
/\bis preserved\b/,
|
|
85
|
+
/\b(?:remains?|stays?) (?:preserved|intact|in place)\b/,
|
|
86
|
+
/\brefuses? to\b/,
|
|
87
|
+
/\bmust still be rejected\b/,
|
|
88
|
+
/\bnever (?:rebound|rebind|launders?|laundered)\b/,
|
|
89
|
+
/\bstays? (?:rejected|blocked)\b/,
|
|
90
|
+
/\bdoes not emit\b/,
|
|
91
|
+
/\bstill emits?\b.{0,60}\binvalid\b/,
|
|
58
92
|
].some((pattern) => pattern.test(normalized));
|
|
59
93
|
}
|
|
60
94
|
const CLAUSE_KEYWORD_OR_PUNCTUATION = /\b(?:but|while|although|however)\b|[,;]/i;
|
|
@@ -66,7 +100,7 @@ function splitClauses(text) {
|
|
|
66
100
|
.map((clause) => clause.trim())
|
|
67
101
|
.filter(Boolean);
|
|
68
102
|
}
|
|
69
|
-
const AFFIRMATIVE_ACTION_VERBS = /\b(?:build(?:s|ing)?|chang(?:e|es|ing)|modif(?:y|ies|ying)|alter(?:s|ing)?|add(?:s|ing)?|introduc(?:e|es|ing)|implement(?:s|ing)?|enabl(?:e|es|ing)|expos(?:e|es|ing|ed)|mutat(?:e|es|ing)|rewrit(?:e|es|ing|ten))\b/;
|
|
103
|
+
const AFFIRMATIVE_ACTION_VERBS = /\b(?:build(?:s|ing)?|chang(?:e|es|ing)|modif(?:y|ies|ying)|alter(?:s|ing)?|add(?:s|ing)?|introduc(?:e|es|ing)|implement(?:s|ing)?|enabl(?:e|es|ing)|expos(?:e|es|ing|ed)|mutat(?:e|es|ing)|rewrit(?:e|es|ing|ten)|remov(?:e|es|ing|ed)|forc(?:e|es|ing))\b/;
|
|
70
104
|
function assertsForbiddenAction(text) {
|
|
71
105
|
return AFFIRMATIVE_ACTION_VERBS.test(normalize(text));
|
|
72
106
|
}
|
|
@@ -78,9 +112,9 @@ function hasAffirmativeDrift(text, outOfScopeItem) {
|
|
|
78
112
|
new RegExp(`^${AFFIRMATIVE_ACTION_VERBS.source}`).test(segment) &&
|
|
79
113
|
clauseMatchesScope(actionSegments[index - 1] ?? '', outOfScopeItem);
|
|
80
114
|
for (const match of segment.matchAll(action)) {
|
|
81
|
-
const before = segment.slice(Math.max(0, match.index -
|
|
115
|
+
const before = segment.slice(Math.max(0, match.index - 80), match.index);
|
|
82
116
|
const after = segment.slice(match.index + match[0].length);
|
|
83
|
-
if (
|
|
117
|
+
if (endsWithNegation(before)) {
|
|
84
118
|
continue;
|
|
85
119
|
}
|
|
86
120
|
if (/^\s+remains? unchanged\b/.test(after)) {
|
|
@@ -102,7 +136,8 @@ function isDirectPhraseMatch(normCriterion, normScope) {
|
|
|
102
136
|
function clauseMatchesScope(clause, outOfScopeItem) {
|
|
103
137
|
const normalizedClause = normalize(clause);
|
|
104
138
|
const normalizedScope = normalize(outOfScopeItem);
|
|
105
|
-
if (normalizedClause
|
|
139
|
+
if (containsKeyword(normalizedClause, normalizedScope) ||
|
|
140
|
+
containsKeyword(normalizedScope, normalizedClause)) {
|
|
106
141
|
return true;
|
|
107
142
|
}
|
|
108
143
|
const scopeKeywords = keywords(outOfScopeItem);
|
|
@@ -116,6 +151,33 @@ function clauseMentionsScope(clause, outOfScopeItem) {
|
|
|
116
151
|
const mentionCandidates = scopeKeywords.length > 0 ? scopeKeywords : [normalize(outOfScopeItem)];
|
|
117
152
|
return mentionCandidates.some((keyword) => containsKeyword(normalizedClause, keyword));
|
|
118
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Bind the affirmative verb to the excluded object — an action verb only
|
|
156
|
+
* counts when a scope keyword sits within ACTION_SCOPE_WINDOW tokens of it.
|
|
157
|
+
*/
|
|
158
|
+
const ACTION_SCOPE_WINDOW = 4;
|
|
159
|
+
function assertsActionNearScope(clause, outOfScopeItem) {
|
|
160
|
+
const tokens = normalize(clause).split(' ');
|
|
161
|
+
const scopeKeywords = keywords(outOfScopeItem);
|
|
162
|
+
const mentionCandidates = scopeKeywords.length > 0 ? scopeKeywords : [normalize(outOfScopeItem)];
|
|
163
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
164
|
+
const token = tokens[i] ?? '';
|
|
165
|
+
if (!AFFIRMATIVE_ACTION_VERBS.test(token)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const before = tokens.slice(Math.max(0, i - 6), i).join(' ');
|
|
169
|
+
if (endsWithNegation(before)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const lo = Math.max(0, i - ACTION_SCOPE_WINDOW);
|
|
173
|
+
const hi = Math.min(tokens.length, i + ACTION_SCOPE_WINDOW + 1);
|
|
174
|
+
const window = tokens.slice(lo, hi).join(' ');
|
|
175
|
+
if (mentionCandidates.some((keyword) => containsKeyword(window, keyword))) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
119
181
|
/**
|
|
120
182
|
* Determine whether a criterion text contradicts an out-of-scope item.
|
|
121
183
|
* Uses substring match first, then keyword overlap as fuzzy fallback.
|
|
@@ -139,7 +201,10 @@ function contradicts(criterionText, outOfScopeItem) {
|
|
|
139
201
|
}
|
|
140
202
|
const scopeAssertsForbiddenAction = assertsForbiddenAction(outOfScopeItem);
|
|
141
203
|
return clauses.some((clause) => clauseMatchesScope(clause, outOfScopeItem) &&
|
|
142
|
-
(!scopeAssertsForbiddenAction ||
|
|
204
|
+
(!scopeAssertsForbiddenAction ||
|
|
205
|
+
(hasConditionalException
|
|
206
|
+
? assertsForbiddenAction(clause)
|
|
207
|
+
: assertsActionNearScope(clause, outOfScopeItem))));
|
|
143
208
|
}
|
|
144
209
|
/**
|
|
145
210
|
* Check acceptance criteria against the spec's outOfScope declarations.
|
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.32",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|