@mjasnikovs/pi-task 0.18.43 → 0.18.45

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(/\s+[—–-]\s+|\s*\|\s*/)[0];
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.43",
3
+ "version": "0.18.45",
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",
@@ -13,8 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc -p tsconfig.build.json",
16
- "lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit",
17
- "test": "cross-env AGENT=1 bun test --isolate src/",
16
+ "lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit && tsc -p scripts/tsconfig.json --noEmit",
17
+ "test": "cross-env AGENT=1 bun test --isolate src/ scripts/",
18
18
  "prepublishOnly": "bun run build"
19
19
  },
20
20
  "peerDependencies": {