@mjasnikovs/pi-task 0.18.44 → 0.18.46
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.
|
@@ -15,8 +15,12 @@ export declare function mandatesTestsInSameChange(decisions: string, spec?: stri
|
|
|
15
15
|
* the whole project, and it is not test INFRASTRUCTURE. A per-feature task that
|
|
16
16
|
* carries "+ tests" never matches (its head names the feature), which is the
|
|
17
17
|
* point: those are the cadence the decision asks for.
|
|
18
|
+
*
|
|
19
|
+
* `spec` is optional only so the detector stays callable on a bare title list;
|
|
20
|
+
* pass it whenever it is available, or unmarked spec echoes read as scope (see
|
|
21
|
+
* `stripSpecEchoes`).
|
|
18
22
|
*/
|
|
19
|
-
export declare function findBatchTestTitles(titles: string[]): number[];
|
|
23
|
+
export declare function findBatchTestTitles(titles: string[], spec?: string): number[];
|
|
20
24
|
/**
|
|
21
25
|
* The spec's own coverage source — the command whose report scopes the sweep.
|
|
22
26
|
*
|
|
@@ -88,9 +88,11 @@ function splitDecisions(title) {
|
|
|
88
88
|
const decisions = DECISIONS_CLAUSE_RE.exec(title);
|
|
89
89
|
return { body, tail: decisions ? title.slice(decisions.index) : '' };
|
|
90
90
|
}
|
|
91
|
+
/** How decompose separates a title's head from its detail segments. */
|
|
92
|
+
const SEGMENT_SPLIT_RE = /\s+[—–-]\s+|\s*\|\s*/;
|
|
91
93
|
/** The head of a title: everything before the " — <detail>" separator. */
|
|
92
94
|
function head(body) {
|
|
93
|
-
return body.split(
|
|
95
|
+
return body.split(SEGMENT_SPLIT_RE)[0];
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
96
98
|
* Remove double-quoted spans — they are QUOTED SPEC TEXT, never the task's own
|
|
@@ -105,6 +107,61 @@ function head(body) {
|
|
|
105
107
|
function stripQuotedSpans(s) {
|
|
106
108
|
return s.replace(/"[^"]*"/g, ' ').replace(/[“][^”]*[”]/g, ' ');
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Normalize for echo comparison: case, markdown emphasis, backticks and all
|
|
112
|
+
* punctuation collapse away, so a title that reflows a spec line across a newline
|
|
113
|
+
* still matches the spec's own wording.
|
|
114
|
+
*/
|
|
115
|
+
function normalizeForEcho(s) {
|
|
116
|
+
return s
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A detail segment shorter than this is not treated as a citation even when it
|
|
123
|
+
* appears in the spec — short phrases ("with screenshot baselines") are shared
|
|
124
|
+
* vocabulary, not provenance, and stripping them would blind the scope check.
|
|
125
|
+
*/
|
|
126
|
+
const MIN_ECHO_CHARS = 40;
|
|
127
|
+
/**
|
|
128
|
+
* Drop detail segments that are VERBATIM spec text.
|
|
129
|
+
*
|
|
130
|
+
* Third variant of the same failure, and the one the syntactic defenses miss:
|
|
131
|
+
* measured live (rep 4, Qwen3.6-27B on the mx5 fixture), the model appended its
|
|
132
|
+
* citation as BARE PROSE — no quotes, no `[source: …]` wrapper — and then
|
|
133
|
+
* repeated it inside a `[source: "…"]` clause:
|
|
134
|
+
*
|
|
135
|
+
* "Implement Login page with phone/password form and tests — **Client/UI:**
|
|
136
|
+
* Playwright `1.61.1` React component tests … — every component/page test
|
|
137
|
+
* captures a screenshot committed as a baseline. [source: "…"]"
|
|
138
|
+
*
|
|
139
|
+
* `stripQuotedSpans` sees no quotes and `splitDecisions` cuts only at the clause,
|
|
140
|
+
* so the borrowed "every component/page" survived as if it were the task's own
|
|
141
|
+
* scope, and a correctly per-change-scoped Login task was DROPPED — the exact
|
|
142
|
+
* deletion this module exists to avoid.
|
|
143
|
+
*
|
|
144
|
+
* The general rule the two earlier guards were reaching for: text the title shares
|
|
145
|
+
* verbatim with the spec is provenance, whoever failed to mark it as such. So the
|
|
146
|
+
* check is semantic, against the spec, rather than one more citation dialect.
|
|
147
|
+
*
|
|
148
|
+
* The HEAD segment is never stripped: it is the task's own claim of what it
|
|
149
|
+
* delivers, and a batch title that happens to quote the spec must still be caught.
|
|
150
|
+
*/
|
|
151
|
+
function stripSpecEchoes(body, specNorm) {
|
|
152
|
+
if (specNorm === '')
|
|
153
|
+
return body;
|
|
154
|
+
const segments = body.split(SEGMENT_SPLIT_RE);
|
|
155
|
+
if (segments.length < 2)
|
|
156
|
+
return body;
|
|
157
|
+
const kept = segments.filter((seg, i) => {
|
|
158
|
+
if (i === 0)
|
|
159
|
+
return true;
|
|
160
|
+
const norm = normalizeForEcho(seg);
|
|
161
|
+
return norm.length < MIN_ECHO_CHARS || !specNorm.includes(norm);
|
|
162
|
+
});
|
|
163
|
+
return kept.join(' — ');
|
|
164
|
+
}
|
|
108
165
|
/** A title whose DELIVERABLE is tests: an authoring verb whose object is tests,
|
|
109
166
|
* with nothing else claimed in between ("Write component and page tests" ✓,
|
|
110
167
|
* "Add listings CRUD + tests" ✗ — the `+` marks tests as an additive constraint
|
|
@@ -133,12 +190,17 @@ const WHOLE_SCOPE_RE = /\b(?:all|every|each|entire|whole|comprehensive)\b[\w\s,/
|
|
|
133
190
|
* the whole project, and it is not test INFRASTRUCTURE. A per-feature task that
|
|
134
191
|
* carries "+ tests" never matches (its head names the feature), which is the
|
|
135
192
|
* point: those are the cadence the decision asks for.
|
|
193
|
+
*
|
|
194
|
+
* `spec` is optional only so the detector stays callable on a bare title list;
|
|
195
|
+
* pass it whenever it is available, or unmarked spec echoes read as scope (see
|
|
196
|
+
* `stripSpecEchoes`).
|
|
136
197
|
*/
|
|
137
|
-
export function findBatchTestTitles(titles) {
|
|
198
|
+
export function findBatchTestTitles(titles, spec = '') {
|
|
199
|
+
const specNorm = normalizeForEcho(spec);
|
|
138
200
|
const out = [];
|
|
139
201
|
for (let i = 0; i < titles.length; i++) {
|
|
140
202
|
const { body } = splitDecisions(titles[i]);
|
|
141
|
-
const scope = stripQuotedSpans(body);
|
|
203
|
+
const scope = stripSpecEchoes(stripQuotedSpans(body), specNorm);
|
|
142
204
|
const h = head(scope);
|
|
143
205
|
if (!TEST_AUTHORING_RE.test(h))
|
|
144
206
|
continue;
|
|
@@ -278,7 +340,7 @@ export function buildSweepTitle(orphaned, coverageSource) {
|
|
|
278
340
|
export function rewriteBatchTestPlan(titles, decisions, spec, requirementQuotes, isCrossCutting) {
|
|
279
341
|
if (!mandatesTestsInSameChange(decisions, spec))
|
|
280
342
|
return { titles, actions: [] };
|
|
281
|
-
const batch = findBatchTestTitles(titles);
|
|
343
|
+
const batch = findBatchTestTitles(titles, spec);
|
|
282
344
|
if (batch.length === 0)
|
|
283
345
|
return { titles, actions: [] };
|
|
284
346
|
const batchSet = new Set(batch);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.46",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|