@mjasnikovs/pi-task 0.14.6 → 0.14.8
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.
|
@@ -29,6 +29,19 @@ import { getParentContextWindow, resolveContextUsage } from './context-usage.js'
|
|
|
29
29
|
const MAX_CLARIFY_QUESTIONS = 8;
|
|
30
30
|
// Matches pi's @-file completion token (a path after @, until whitespace).
|
|
31
31
|
const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
|
|
32
|
+
// Trailing punctuation a user naturally types AFTER an @-mention when it sits in
|
|
33
|
+
// prose — "Implement @design.md, reuse…" or "see @spec.md." — which the greedy
|
|
34
|
+
// [^\s]+ above would otherwise swallow into the path. Left unstripped, the
|
|
35
|
+
// resulting "design.md," resolves to no file, expansion is silently skipped, and
|
|
36
|
+
// the planner reasons over a one-line "Implement @design.md" with NO spec inline
|
|
37
|
+
// → it fabricates generic questions/tasks the spec never called for (validated:
|
|
38
|
+
// a stray comma turned a 32KB design into a contentless prompt). None of these
|
|
39
|
+
// chars are legitimate trailing characters of a referenced doc path.
|
|
40
|
+
const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
|
|
41
|
+
/** The cleaned path token of an @-mention: greedy match minus trailing prose punctuation. */
|
|
42
|
+
function mentionPath(token) {
|
|
43
|
+
return token.replace(MENTION_TRAILING_PUNCT, '');
|
|
44
|
+
}
|
|
32
45
|
/**
|
|
33
46
|
* Expand any @file references in the feature text by appending each referenced
|
|
34
47
|
* file's contents, so the planning children (clarify, decompose) always see the
|
|
@@ -42,8 +55,8 @@ export async function expandFeatureMentions(cwd, feature) {
|
|
|
42
55
|
const seen = new Set();
|
|
43
56
|
const blocks = [];
|
|
44
57
|
for (const m of feature.matchAll(MENTION_RE)) {
|
|
45
|
-
const rel = m[1];
|
|
46
|
-
if (seen.has(rel))
|
|
58
|
+
const rel = mentionPath(m[1]);
|
|
59
|
+
if (rel === '' || seen.has(rel))
|
|
47
60
|
continue;
|
|
48
61
|
seen.add(rel);
|
|
49
62
|
try {
|
|
@@ -68,8 +81,8 @@ export async function readableMentions(cwd, feature) {
|
|
|
68
81
|
const out = [];
|
|
69
82
|
const seen = new Set();
|
|
70
83
|
for (const m of feature.matchAll(MENTION_RE)) {
|
|
71
|
-
const rel = m[1];
|
|
72
|
-
if (seen.has(rel))
|
|
84
|
+
const rel = mentionPath(m[1]);
|
|
85
|
+
if (rel === '' || seen.has(rel))
|
|
73
86
|
continue;
|
|
74
87
|
seen.add(rel);
|
|
75
88
|
try {
|
package/dist/task/phases.js
CHANGED
|
@@ -6,6 +6,7 @@ import { docsFocused } from '../workers/docs-core.js';
|
|
|
6
6
|
import { fetchFocused } from '../workers/fetch-core.js';
|
|
7
7
|
import { formatNpmVersionSection } from '../workers/npm-version.js';
|
|
8
8
|
import { runWorker } from '../workers/pi-worker-core.js';
|
|
9
|
+
import { findPhantomImports, formatApiCorrections } from '../workers/phantom-imports.js';
|
|
9
10
|
import { search as defaultSearch } from '../workers/search-core.js';
|
|
10
11
|
import { extractEnrichTargets } from './enrichment.js';
|
|
11
12
|
import { getFileInventory } from './file-inventory.js';
|
|
@@ -663,6 +664,17 @@ export const PHASES = [
|
|
|
663
664
|
const tVerify = Date.now();
|
|
664
665
|
const out = await phaseVerifyTooling(d, rawResearch);
|
|
665
666
|
d.recordSubStep?.('verify-tooling', Date.now() - tVerify);
|
|
667
|
+
// Deterministically verify every runtime builtin specifier the refined
|
|
668
|
+
// task names (`bun:sql`, `node:…`) against the installed types. A doc can
|
|
669
|
+
// confidently name a module that does not exist; left unchecked it rides
|
|
670
|
+
// through every phase and the implementer fabricates a `declare module`
|
|
671
|
+
// shim to compile it. Append the corrections so compose folds them into
|
|
672
|
+
// CONSTRAINTS. No LLM cost and silent when nothing is wrong.
|
|
673
|
+
const corrections = formatApiCorrections(findPhantomImports(p.refined, d.cwd));
|
|
674
|
+
if (corrections) {
|
|
675
|
+
d.logDebug?.(`phantom imports flagged:\n${corrections}`);
|
|
676
|
+
return `${out}\n\n${corrections}`;
|
|
677
|
+
}
|
|
666
678
|
return out;
|
|
667
679
|
}
|
|
668
680
|
},
|
package/dist/task/prompts.js
CHANGED
|
@@ -309,6 +309,8 @@ VERIFY must exercise the surface area the task actually touches. Draw VERIFY com
|
|
|
309
309
|
|
|
310
310
|
If TOOLING is empty for a category the change clearly touches, still include the best-effort standard command for that ecosystem (e.g. \`npx tsc --noEmit\` for a TS repo with no script) and note that the receiving agent may need to install it.
|
|
311
311
|
|
|
312
|
+
If the research contains an "API CORRECTIONS" section, it is AUTHORITATIVE — each line was verified against the installed types. For every correction: use the import it prescribes, never the specifier it marks non-existent, and add a CONSTRAINT recording it verbatim (e.g. Use \`import { sql } from "bun"\`; \`bun:sql\` is not a module — do not import it or declare a module for it). This overrides any conflicting identifier carried up from the refined task or the spec doc.
|
|
313
|
+
|
|
312
314
|
Refined task:
|
|
313
315
|
${refined}
|
|
314
316
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface PhantomImport {
|
|
2
|
+
/** The bad specifier exactly as written, e.g. `bun:sql`. */
|
|
3
|
+
spec: string;
|
|
4
|
+
/** Real `<runtime>:` modules the types DO declare (evidence for the correction). */
|
|
5
|
+
realModules: string[];
|
|
6
|
+
/** Corrective guidance to inject into the spec. */
|
|
7
|
+
suggestion: string;
|
|
8
|
+
}
|
|
9
|
+
/** Every distinct runtime-namespace specifier mentioned in the text, in first-seen order. */
|
|
10
|
+
export declare function extractRuntimeSpecifiers(text: string): string[];
|
|
11
|
+
export interface RuntimeImportVerdict {
|
|
12
|
+
spec: string;
|
|
13
|
+
real: boolean;
|
|
14
|
+
/** When phantom: a base-module symbol matching the submodule leaf, if the types
|
|
15
|
+
* declare one (so the correction can name the real import). */
|
|
16
|
+
baseSymbol: string | null;
|
|
17
|
+
/** Real `<runtime>:` modules declared in the type text. */
|
|
18
|
+
realModules: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pure verdict over a runtime's concatenated type text — no I/O, so the rule is
|
|
22
|
+
* unit-testable with synthetic declarations.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyRuntimeImport(spec: string, runtime: string, sub: string, typeText: string): RuntimeImportVerdict;
|
|
25
|
+
/** Default loader: resolve the runtime's installed types and read their .d.ts. */
|
|
26
|
+
export declare function loadRuntimeTypeText(runtime: string, cwd: string): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Scan `text` for runtime-namespace specifiers and return the ones the installed
|
|
29
|
+
* types do not declare. `loadText` is injectable for tests; in production it reads
|
|
30
|
+
* the runtime's type package. A runtime whose types can't be loaded is skipped
|
|
31
|
+
* (we never flag what we can't verify), so this is silent when types are absent.
|
|
32
|
+
*/
|
|
33
|
+
export declare function findPhantomImports(text: string, cwd: string, loadText?: (runtime: string, cwd: string) => string | null): PhantomImport[];
|
|
34
|
+
/** Render flagged phantoms as an authoritative research section, or '' if none. */
|
|
35
|
+
export declare function formatApiCorrections(phantoms: PhantomImport[]): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect phantom runtime imports — `bun:sql`, `node:nope`, etc. — referenced in a
|
|
3
|
+
* task/spec but NOT actually declared by the runtime's installed types.
|
|
4
|
+
*
|
|
5
|
+
* A real `<runtime>:<sub>` builtin appears as `declare module "<runtime>:<sub>"`
|
|
6
|
+
* in the runtime's type files (bun-types declares bun:sqlite/bun:test/bun:ffi/…;
|
|
7
|
+
* @types/node declares node:fs/…). A specifier with no such declaration is a
|
|
8
|
+
* hallucination — the symbol, if it exists at all, lives on the base module. This
|
|
9
|
+
* is the exact `bun:sql` failure class: a design doc invented `bun:sql`, every
|
|
10
|
+
* phase echoed it, and the implementer fabricated a `declare module "bun:sql"`
|
|
11
|
+
* shim to make it compile. Verifying the specifier against the installed types
|
|
12
|
+
* catches it deterministically, with no false positives and no LLM call.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { resolvePackage, splitRuntimeNamespace, detectTypesRedirect, typesPackageName, hasTypeFiles, isDtsFile, ResolveError } from './docs-resolve.js';
|
|
17
|
+
// Runtime builtin specifiers as they appear in prose/code: `bun:sql`,
|
|
18
|
+
// `node:fs/promises`. Bounded to the runtimes splitRuntimeNamespace accepts.
|
|
19
|
+
const SPEC_RE = /\b(?:bun|node|deno):[a-z0-9][a-z0-9/_-]*/gi;
|
|
20
|
+
const DECLARE_MODULE_RE = /declare module "([^"]+)"/g;
|
|
21
|
+
/** Every distinct runtime-namespace specifier mentioned in the text, in first-seen order. */
|
|
22
|
+
export function extractRuntimeSpecifiers(text) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const m of text.matchAll(SPEC_RE)) {
|
|
25
|
+
// Normalise: drop a trailing punctuation the word boundary may include is
|
|
26
|
+
// already excluded by the class; lowercase the runtime only (subpaths are
|
|
27
|
+
// case-sensitive). splitRuntimeNamespace lowercases the runtime itself.
|
|
28
|
+
if (!seen.has(m[0]))
|
|
29
|
+
seen.add(m[0]);
|
|
30
|
+
}
|
|
31
|
+
return [...seen];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Pure verdict over a runtime's concatenated type text — no I/O, so the rule is
|
|
35
|
+
* unit-testable with synthetic declarations.
|
|
36
|
+
*/
|
|
37
|
+
export function classifyRuntimeImport(spec, runtime, sub, typeText) {
|
|
38
|
+
const declared = new Set();
|
|
39
|
+
for (const m of typeText.matchAll(DECLARE_MODULE_RE))
|
|
40
|
+
declared.add(m[1]);
|
|
41
|
+
const real = declared.has(spec);
|
|
42
|
+
const realModules = [...declared].filter(d => d.startsWith(`${runtime}:`)).sort();
|
|
43
|
+
let baseSymbol = null;
|
|
44
|
+
if (!real) {
|
|
45
|
+
const leaf = sub.split('/').pop() ?? sub;
|
|
46
|
+
// Does the base runtime module declare a symbol named like the submodule
|
|
47
|
+
// leaf (e.g. `bun:sql` → `const sql` / `class SQL` in `declare module "bun"`)?
|
|
48
|
+
// Case-insensitive so `sql` matches the `SQL` class too.
|
|
49
|
+
const re = new RegExp(`\\b(?:const|class|function|let|var|namespace|interface|type)\\s+(${escapeRe(leaf)})\\b`, 'i');
|
|
50
|
+
const m = re.exec(typeText);
|
|
51
|
+
baseSymbol = m ? m[1] : null;
|
|
52
|
+
}
|
|
53
|
+
return { spec, real, baseSymbol, realModules };
|
|
54
|
+
}
|
|
55
|
+
function escapeRe(s) {
|
|
56
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
function suggestionFor(v, runtime) {
|
|
59
|
+
if (v.baseSymbol) {
|
|
60
|
+
return (`\`${v.spec}\` is NOT a module — the symbol is exported from the base runtime `
|
|
61
|
+
+ `module: use \`import { ${v.baseSymbol} } from "${runtime}"\` (never import from `
|
|
62
|
+
+ `\`${v.spec}\` and never declare a module for it).`);
|
|
63
|
+
}
|
|
64
|
+
const list = v.realModules.length > 0 ?
|
|
65
|
+
` The real \`${runtime}:\` modules are: ${v.realModules.join(', ')}.`
|
|
66
|
+
: '';
|
|
67
|
+
return (`\`${v.spec}\` is NOT a real module.${list} Import the needed symbol from `
|
|
68
|
+
+ `"${runtime}" or verify the correct specifier with pi-worker-docs; do not declare a module for it.`);
|
|
69
|
+
}
|
|
70
|
+
/** Sync resolution of a runtime to the package that actually holds its type
|
|
71
|
+
* declarations (bun -> @types/bun -> bun-types), bounded to a few hops. No
|
|
72
|
+
* auto-install: a runtime whose types aren't installed simply can't be verified
|
|
73
|
+
* (returns null), so we never flag what we can't prove. */
|
|
74
|
+
function resolveRuntimeTypesRoot(runtime, cwd) {
|
|
75
|
+
let cur;
|
|
76
|
+
try {
|
|
77
|
+
cur = resolvePackage(runtime, cwd);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err instanceof ResolveError)
|
|
81
|
+
return null;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
const visited = new Set([cur.name, runtime]);
|
|
85
|
+
for (let hop = 0; hop < 3; hop++) {
|
|
86
|
+
let next = detectTypesRedirect(cur);
|
|
87
|
+
if (next && visited.has(next))
|
|
88
|
+
next = null;
|
|
89
|
+
if (!next && !hasTypeFiles(cur.root)) {
|
|
90
|
+
const types = typesPackageName(cur.name);
|
|
91
|
+
if (types && !visited.has(types))
|
|
92
|
+
next = types;
|
|
93
|
+
}
|
|
94
|
+
if (!next)
|
|
95
|
+
break;
|
|
96
|
+
visited.add(next);
|
|
97
|
+
try {
|
|
98
|
+
cur = resolvePackage(next, cwd);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return hasTypeFiles(cur.root) ? cur.root : null;
|
|
105
|
+
}
|
|
106
|
+
const MAX_TYPE_BYTES = 4_000_000;
|
|
107
|
+
function readRuntimeTypeText(root) {
|
|
108
|
+
const parts = [];
|
|
109
|
+
let total = 0;
|
|
110
|
+
const stack = [root];
|
|
111
|
+
while (stack.length) {
|
|
112
|
+
const dir = stack.pop();
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (entry.name === 'node_modules')
|
|
122
|
+
continue;
|
|
123
|
+
const full = path.join(dir, entry.name);
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
stack.push(full);
|
|
126
|
+
}
|
|
127
|
+
else if (entry.isFile() && isDtsFile(entry.name)) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
130
|
+
total += raw.length;
|
|
131
|
+
parts.push(raw);
|
|
132
|
+
if (total >= MAX_TYPE_BYTES)
|
|
133
|
+
return parts.join('\n');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// unreadable file — skip
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return parts.join('\n');
|
|
142
|
+
}
|
|
143
|
+
/** Default loader: resolve the runtime's installed types and read their .d.ts. */
|
|
144
|
+
export function loadRuntimeTypeText(runtime, cwd) {
|
|
145
|
+
const root = resolveRuntimeTypesRoot(runtime, cwd);
|
|
146
|
+
if (!root)
|
|
147
|
+
return null;
|
|
148
|
+
const text = readRuntimeTypeText(root);
|
|
149
|
+
return text.length > 0 ? text : null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Scan `text` for runtime-namespace specifiers and return the ones the installed
|
|
153
|
+
* types do not declare. `loadText` is injectable for tests; in production it reads
|
|
154
|
+
* the runtime's type package. A runtime whose types can't be loaded is skipped
|
|
155
|
+
* (we never flag what we can't verify), so this is silent when types are absent.
|
|
156
|
+
*/
|
|
157
|
+
export function findPhantomImports(text, cwd, loadText = loadRuntimeTypeText) {
|
|
158
|
+
const out = [];
|
|
159
|
+
const typeTextByRuntime = new Map();
|
|
160
|
+
for (const spec of extractRuntimeSpecifiers(text)) {
|
|
161
|
+
const ns = splitRuntimeNamespace(spec);
|
|
162
|
+
if (!ns)
|
|
163
|
+
continue;
|
|
164
|
+
if (!typeTextByRuntime.has(ns.runtime)) {
|
|
165
|
+
typeTextByRuntime.set(ns.runtime, loadText(ns.runtime, cwd));
|
|
166
|
+
}
|
|
167
|
+
const typeText = typeTextByRuntime.get(ns.runtime);
|
|
168
|
+
if (!typeText)
|
|
169
|
+
continue;
|
|
170
|
+
const verdict = classifyRuntimeImport(spec, ns.runtime, ns.sub, typeText);
|
|
171
|
+
if (verdict.real)
|
|
172
|
+
continue;
|
|
173
|
+
out.push({
|
|
174
|
+
spec,
|
|
175
|
+
realModules: verdict.realModules,
|
|
176
|
+
suggestion: suggestionFor(verdict, ns.runtime)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** Render flagged phantoms as an authoritative research section, or '' if none. */
|
|
182
|
+
export function formatApiCorrections(phantoms) {
|
|
183
|
+
if (phantoms.length === 0)
|
|
184
|
+
return '';
|
|
185
|
+
const lines = phantoms.map(p => ` - ${p.suggestion}`);
|
|
186
|
+
return `API CORRECTIONS\n${lines.join('\n')}`;
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|