@mjasnikovs/pi-task 0.23.1 → 0.24.0

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.
@@ -31,7 +31,18 @@ export declare function keepGroundedRequirements(entries: RequirementEntry[], so
31
31
  * never the model-authored anchor text. Without `sourceDoc` (or for quotes that
32
32
  * cannot be located) the fill degrades to the old given-order behavior.
33
33
  */
34
- export declare function capRequirements(entries: RequirementEntry[], passages: string[], sourceDoc?: string): RequirementEntry[];
34
+ export declare function capRequirements(entries: RequirementEntry[], passages: string[], sourceDoc?: string,
35
+ /** A/B seam: `false` reproduces the pre-budget rule, so an offline harness can
36
+ * score the shipped rule against the one it replaced without transcribing
37
+ * sectionFairFill and letting the copy drift. Production never passes it. */
38
+ deprioritiseLowValue?: boolean): RequirementEntry[];
39
+ /**
40
+ * Quotes that pass the grounding guard (verbatim substring of the doc) but state
41
+ * no obligation. There is no obligation test anywhere else in the pipeline —
42
+ * `keepGroundedRequirements` only checks the quote really appears in the source,
43
+ * so any sentence at all survives it and precision rests entirely on the model.
44
+ */
45
+ export declare function isLowValueQuote(quote: string): boolean;
35
46
  /**
36
47
  * DETERMINISTIC RECALL FLOOR (same medicine as the launch-contract checklist):
37
48
  * paragraphs carrying an obligation marker (word-bounded "required"/"must").
@@ -98,7 +98,11 @@ export function keepGroundedRequirements(entries, sourceDoc) {
98
98
  * never the model-authored anchor text. Without `sourceDoc` (or for quotes that
99
99
  * cannot be located) the fill degrades to the old given-order behavior.
100
100
  */
101
- export function capRequirements(entries, passages, sourceDoc) {
101
+ export function capRequirements(entries, passages, sourceDoc,
102
+ /** A/B seam: `false` reproduces the pre-budget rule, so an offline harness can
103
+ * score the shipped rule against the one it replaced without transcribing
104
+ * sectionFairFill and letting the copy drift. Production never passes it. */
105
+ deprioritiseLowValue = true) {
102
106
  if (entries.length <= MAX_REQUIREMENTS)
103
107
  return entries;
104
108
  const norms = passages.map(normalise);
@@ -109,7 +113,114 @@ export function capRequirements(entries, passages, sourceDoc) {
109
113
  const marked = entries.filter(covers);
110
114
  const rest = entries.filter(e => !covers(e));
111
115
  const budget = MAX_REQUIREMENTS - Math.min(marked.length, MAX_REQUIREMENTS);
112
- return [...marked.slice(0, MAX_REQUIREMENTS), ...sectionFairFill(rest, budget, sourceDoc)];
116
+ // The low-value filter applies to the UNMARKED remainder only. Quoting an
117
+ // obligation-marked passage is the pipeline's existing, validated evidence
118
+ // that a quote states an obligation, and it outranks any lexical heuristic:
119
+ // "MUST log every request" is 22 characters and every length-based rule reads
120
+ // it as a fragment. Filtering ahead of the marked/rest split deleted it.
121
+ //
122
+ // INSURANCE, not a measured win on mx5: across both 30-run pools exactly one
123
+ // distinct quote per pool is low-value AND marked, and it is a genuinely
124
+ // truncated one. The layering matters for specs whose obligations are SHORT,
125
+ // which mx5's are not. Do not cite it as the reason tail coverage holds —
126
+ // tail coverage is identical with the filter applied before the split.
127
+ const pool = deprioritiseLowValue ? budgetedByObligation(rest, budget, sourceDoc) : rest;
128
+ return [...marked.slice(0, MAX_REQUIREMENTS), ...sectionFairFill(pool, budget, sourceDoc)];
129
+ }
130
+ /** Longest a dependency-pin row can be before it is presumed to carry an
131
+ * obligation after the pin. Real pins in the measured corpus run 32..48 chars;
132
+ * the pin-prefixed lines that DO obligate ("TypeScript `6.0.3` — one strict
133
+ * `tsconfig.json`: `strict`, `noUncheckedIndexedAccess`, …") run 130..260. */
134
+ const MAX_PIN_LENGTH = 80;
135
+ /** Cut mid-expression: an unbalanced fence or bracket, or a trailing separator.
136
+ * NOT `;` — a complete clause legitimately ends with one, and dropping on `;`
137
+ * discarded a runnable `lint` = `prettier … && eslint … && tsc --noEmit` line. */
138
+ function isTruncatedQuote(q) {
139
+ if ((q.match(/`/g) ?? []).length % 2 === 1)
140
+ return true;
141
+ for (const [open, close] of [
142
+ ['(', ')'],
143
+ ['[', ']'],
144
+ ['{', '}']
145
+ ]) {
146
+ const o = (q.match(new RegExp(`\\${open}`, 'g')) ?? []).length;
147
+ const c = (q.match(new RegExp(`\\${close}`, 'g')) ?? []).length;
148
+ if (o > c)
149
+ return true;
150
+ }
151
+ return /[,:([{]\s*$/.test(q.trim());
152
+ }
153
+ /** A bare version row — data, not an obligation. Length-gated, see MAX_PIN_LENGTH. */
154
+ function isDependencyPin(q) {
155
+ const t = q.trim().replace(/^\*\*|\*\*$/g, '');
156
+ if (t.length > MAX_PIN_LENGTH)
157
+ return false;
158
+ return /^\**[`*]?[\w@/-]+[`*]?\**\s*[`']?\d+\.\d+/.test(t);
159
+ }
160
+ /** A DDL/schema row: `col type …`. */
161
+ function isSchemaRow(q) {
162
+ return /^\s*[\w_]+\s+(?:uuid|text|int|integer|bigint|boolean|timestamptz|bytea|jsonb|numeric|smallint)\b/i.test(q);
163
+ }
164
+ /** Too short to state an obligation. MIN_QUOTE_LENGTH (6) admits "Contact
165
+ * seller"; a clause needs a subject and a predicate. */
166
+ function isQuoteFragment(q) {
167
+ const t = q.trim();
168
+ return t.length < 25 || t.split(/\s+/).length < 4;
169
+ }
170
+ /**
171
+ * Quotes that pass the grounding guard (verbatim substring of the doc) but state
172
+ * no obligation. There is no obligation test anywhere else in the pipeline —
173
+ * `keepGroundedRequirements` only checks the quote really appears in the source,
174
+ * so any sentence at all survives it and precision rests entirely on the model.
175
+ */
176
+ export function isLowValueQuote(quote) {
177
+ return (isTruncatedQuote(quote)
178
+ || isDependencyPin(quote)
179
+ || isSchemaRow(quote)
180
+ || isQuoteFragment(quote));
181
+ }
182
+ /**
183
+ * Deprioritise obligation-free quotes, but only as far as the BUDGET requires.
184
+ *
185
+ * Measured (mx5, 20402-char spec, two independent 30-run extraction pools): the
186
+ * extractor's single-pass yield swings 20..160 for byte-identical input, and the
187
+ * padding crowds real obligations out of the 40 that ship — high-yield runs land
188
+ * FEWER critical obligations than low-yield ones. Critical obligations reaching
189
+ * the shipped list go 8.50 → 9.37 of 16 on the design pool (10 runs better, 0
190
+ * worse, p=0.0020) and 7.70 → 8.43 on the confirmation pool (12 / 0, p=0.0005).
191
+ *
192
+ * BUDGETED, not absolute. Below the cap no slot is contested, so dropping there
193
+ * destroys information and buys nothing; a run that filtered 55 quotes down to 20
194
+ * lost its only carrier of the Argon2id obligation — a DDL row that was correctly
195
+ * classified as one — while 20 slots sat empty. So the low-value entries come back
196
+ * in source-doc order until the list reaches the cap.
197
+ *
198
+ * Absolute filtering scored marginally higher on raw count (24 gains vs 22 across
199
+ * both pools) and was rejected anyway: its extra gains are one more obligation in
200
+ * an already-populated list, while its one loss is an obligation vanishing from a
201
+ * run outright. Those are not the same size of mistake.
202
+ *
203
+ * Doc order for the restore is the neutral choice: which entries return only
204
+ * matters when more were dropped than there are free slots, and ordering by
205
+ * anything fitted to an observed loss would be tuning the rule to one pool.
206
+ */
207
+ function budgetedByObligation(entries, budget, sourceDoc) {
208
+ const keep = entries.filter(e => !isLowValueQuote(e.quote));
209
+ if (keep.length >= budget)
210
+ return keep;
211
+ const at = (e) => {
212
+ if (!sourceDoc)
213
+ return Number.MAX_SAFE_INTEGER;
214
+ const i = sourceDoc.indexOf(e.quote.trim());
215
+ return i < 0 ? Number.MAX_SAFE_INTEGER : i;
216
+ };
217
+ const restored = entries
218
+ .filter(e => isLowValueQuote(e.quote))
219
+ .map((e, given) => ({ e, at: at(e), given }))
220
+ .sort((x, y) => x.at - y.at || x.given - y.given)
221
+ .slice(0, budget - keep.length)
222
+ .map(x => x.e);
223
+ return [...keep, ...restored];
113
224
  }
114
225
  /** The doc split into heading-delimited sections, each pre-normalised for
115
226
  * containment tests. Text before the first heading is its own section. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
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",