@mjasnikovs/pi-task 0.18.1 → 0.18.3

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Deterministic synthesized-wiring scanner for a composed spec (run-8 F3, gen side).
3
+ *
4
+ * F3 (the dominant run-8 shipped defect): refine/compose invent a "uniform" wiring
5
+ * table — one module → one mount prefix, `/api/<x>` → `<x>Routes` for every module —
6
+ * though the design pins ENDPOINTS, not mounts, and one module's pinned endpoints do
7
+ * NOT all sit under a single prefix (photos: `POST /api/listings/:id/photos` AND
8
+ * `GET/DELETE /api/photos/:id`). Mounting that module at `/api/photos` double-prefixes
9
+ * the upload; consumers follow the pinned paths, assembly follows the invented table,
10
+ * the seam ships broken. See [[contract-registry-f3]] (#4, the verify-side + registry
11
+ * lever) — this is its GENERATION-side complement.
12
+ *
13
+ * A/B-measured on the live 27B (F3 critique trap): the CROSS-SLICE CONTRACTS registry
14
+ * is NECESSARY but the prompt+registry alone is a WEAK catcher (A/B arms 0/8, +registry
15
+ * only 1/8) — the model's attention goes to the obvious VERIFY weakness and it rarely
16
+ * does the path-composition reasoning even with the facts in front of it. The reliable
17
+ * lever is the SAME probe+rule pattern as [[skip-escape-scanner-f2]] / [[verify-
18
+ * substitution-ab]]: a deterministic finding that NAMES the exact synthesized mappings
19
+ * and juxtaposes the verbatim pinned facts, forcing focused reconciliation.
20
+ *
21
+ * This scanner does NOT decide which mapping is wrong — that needs routing-composition
22
+ * knowledge (a forbidden stack assumption). It surfaces every mapping that (a) is not a
23
+ * verbatim substring of the design/registry (so it is INFERRED, not cited) AND (b) touches
24
+ * a pinned cross-slice boundary (an operand appears in the registry) — i.e. it reshapes a
25
+ * shared contract. The 4 coincidentally-correct mappings are surfaced too, but framed as
26
+ * "reconcile each; keep the conforming ones" — the LLM decides, informed. Pure text/
27
+ * substring analysis; no stack, framework, or routing assumptions. Empty registry (single
28
+ * `/task`, or no shared boundary) ⇒ no-op.
29
+ */
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ /** Mapping arrows a wiring/mount table uses across notations. A colon is deliberately
33
+ * NOT an arrow — it matches endpoint params (`/photos/:id`), section headers, and prose,
34
+ * which would drown the signal. */
35
+ const ARROW = '(?:→|->|=>|⇒|↦)';
36
+ /** A `left <arrow> right` mapping on one line, tolerating a leading list bullet. */
37
+ const MAPPING_LINE_RE = new RegExp(`^[ \\t]*[-*]?[ \\t]*(.+?)[ \\t]*${ARROW}[ \\t]*(.+?)[ \\t]*$`);
38
+ /** An operand shorter than this is too generic to anchor a boundary match. */
39
+ const MIN_OPERAND_LENGTH = 4;
40
+ /** Collapse whitespace + lowercase; drop markdown backticks so quoting formatting
41
+ * differences don't defeat the substring match. Mirrors contracts.ts's normalise. */
42
+ function normalise(s) {
43
+ return s.replace(/`/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
44
+ }
45
+ /**
46
+ * Find synthesized wiring mappings in `spec`: `A <arrow> B` lines whose whole mapping
47
+ * is NOT a verbatim substring of `grounding` (design ∪ registry) yet an operand appears
48
+ * in `registry` (so it reshapes a pinned cross-slice boundary). Returns [] when the
49
+ * registry is empty (no shared contracts to reshape) — the whole check is a no-op then.
50
+ */
51
+ export function findSynthesizedWiring(spec, grounding, registry) {
52
+ if (registry.trim().length === 0)
53
+ return [];
54
+ const groundHay = normalise(grounding + '\n' + registry);
55
+ const regHay = normalise(registry);
56
+ const found = [];
57
+ const seen = new Set();
58
+ for (const rawLine of spec.split('\n')) {
59
+ const m = MAPPING_LINE_RE.exec(rawLine);
60
+ if (!m)
61
+ continue;
62
+ const from = m[1].trim();
63
+ const to = m[2].trim();
64
+ const line = rawLine.replace(/^[ \t]*[-*][ \t]*/, '').trim();
65
+ const key = normalise(line);
66
+ if (key.length === 0 || seen.has(key))
67
+ continue;
68
+ // Cited, not synthesized: the whole mapping appears verbatim in the source.
69
+ if (groundHay.includes(key))
70
+ continue;
71
+ // Only a mapping that touches a PINNED shared boundary is an F3 suspect — this
72
+ // is the crisp discriminator that keeps internal-flow prose ("input → output")
73
+ // out. An operand (long enough to be specific) must appear in the registry.
74
+ const touchesBoundary = [from, to].some(op => {
75
+ const n = normalise(op);
76
+ return n.length >= MIN_OPERAND_LENGTH && regHay.includes(n);
77
+ });
78
+ if (!touchesBoundary)
79
+ continue;
80
+ seen.add(key);
81
+ found.push({ line, from: from.replace(/`/g, ''), to: to.replace(/`/g, '') });
82
+ }
83
+ return found;
84
+ }
85
+ /**
86
+ * Render findings as the critique probe (probe+rule pattern): NAME the inferred
87
+ * mappings and juxtapose the verbatim pinned facts, then instruct focused
88
+ * reconciliation. Deliberately does NOT accuse a specific mapping — the model decides
89
+ * which (if any) fails to reproduce a pinned fact. Empty findings ⇒ '' (no block).
90
+ */
91
+ export function wiringProbeText(findings, registry) {
92
+ if (findings.length === 0)
93
+ return '';
94
+ return [
95
+ 'SYNTHESIZED WIRING (deterministic finding) — the spec states these connect-the-',
96
+ 'modules mappings that are NOT quoted verbatim from the design (they are INFERRED,',
97
+ 'not cited) and that touch a pinned cross-slice boundary:',
98
+ ...findings.map((f, i) => ` ${i + 1}. ${f.line}`),
99
+ 'The design pins these interface FACTS instead (verbatim, authoritative):',
100
+ ...registry
101
+ .trim()
102
+ .split('\n')
103
+ .filter(l => l.trim().length > 0)
104
+ .map(l => ` - ${l.trim()}`),
105
+ 'For EACH mapping above, confirm it REPRODUCES the pinned facts EXACTLY. A module',
106
+ 'whose pinned facts do NOT all sit under the single prefix it is mapped to CANNOT be',
107
+ 'wired that way without breaking a path — that is a SEAM BUG: name the mapping and the',
108
+ 'pinned fact it fails to produce. KEEP every mapping that does reproduce its facts; do',
109
+ 'NOT alter a conforming one. If a boundary detail is genuinely unpinned, leave it',
110
+ 'unspecified rather than inventing a mapping.'
111
+ ].join('\n');
112
+ }
113
+ /**
114
+ * Render findings as a critique-rewrite defect block (fed into the FOCUS list): the
115
+ * rewrite must reconcile each mapping against the pinned facts and correct only the
116
+ * one(s) that break. Mirrors skipEscapeDefectText's shape.
117
+ */
118
+ export function wiringDefectText(findings, registry) {
119
+ return wiringProbeText(findings, registry);
120
+ }
121
+ // An @-file mention in a spec ("@DESIGN/foo.md"), minus trailing prose punctuation.
122
+ // Mirrors phantom-imports' mention rules so grounding sees the same source docs.
123
+ const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
124
+ const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
125
+ /**
126
+ * Concatenate the design/spec docs the given texts @-reference (best-effort, readable
127
+ * files only) as extra grounding for findSynthesizedWiring — so a mapping the design
128
+ * states verbatim is treated as CITED, not synthesized. Unreadable/absent mentions are
129
+ * skipped; returns '' when nothing resolves.
130
+ */
131
+ export function readReferencedDocs(cwd, ...texts) {
132
+ const seen = new Set();
133
+ const parts = [];
134
+ for (const text of texts) {
135
+ for (const m of text.matchAll(MENTION_RE)) {
136
+ const rel = m[1].replace(MENTION_TRAILING_PUNCT, '');
137
+ if (rel === '' || seen.has(rel))
138
+ continue;
139
+ seen.add(rel);
140
+ try {
141
+ parts.push(fs.readFileSync(path.resolve(cwd, rel), 'utf8'));
142
+ }
143
+ catch {
144
+ // not a readable file — skip
145
+ }
146
+ }
147
+ }
148
+ return parts.join('\n');
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
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",