@mjasnikovs/pi-task 0.39.4 → 0.40.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.
- package/README.md +20 -5
- package/dist/task/auto-orchestrator.d.ts +36 -0
- package/dist/task/auto-orchestrator.js +43 -6
- package/dist/task/cancel-points.d.ts +34 -6
- package/dist/task/cancel-points.js +62 -10
- package/dist/task/child-status.js +13 -1
- package/dist/task/context-attribution.js +18 -6
- package/dist/task/external-context.d.ts +8 -1
- package/dist/task/external-context.js +52 -4
- package/dist/task/orchestrator.js +47 -5
- package/dist/task/phases.js +2 -1
- package/dist/task/plan-orchestrator.js +10 -1
- package/dist/task/prompts.d.ts +7 -1
- package/dist/task/prompts.js +14 -4
- package/dist/task/research-worker.js +11 -0
- package/dist/task/run-bracket.js +13 -1
- package/dist/task/task-gates.js +34 -0
- package/dist/workers/docs-cache.js +50 -3
- package/dist/workers/docs-chunk.d.ts +6 -3
- package/dist/workers/docs-chunk.js +8 -5
- package/dist/workers/docs-core.d.ts +27 -3
- package/dist/workers/docs-core.js +104 -41
- package/dist/workers/docs-ecosystems.d.ts +173 -0
- package/dist/workers/docs-ecosystems.js +449 -0
- package/dist/workers/docs-index.d.ts +2 -1
- package/dist/workers/docs-index.js +55 -27
- package/dist/workers/docs-project.d.ts +10 -0
- package/dist/workers/docs-project.js +86 -24
- package/dist/workers/docs-resolve.d.ts +6 -1
- package/dist/workers/docs-resolve.js +4 -3
- package/dist/workers/docs-retrieve.d.ts +2 -0
- package/dist/workers/docs-retrieve.js +11 -11
- package/dist/workers/eco-cargo.d.ts +115 -0
- package/dist/workers/eco-cargo.js +793 -0
- package/dist/workers/eco-hackage.d.ts +93 -0
- package/dist/workers/eco-hackage.js +508 -0
- package/dist/workers/npm-version.d.ts +5 -3
- package/dist/workers/npm-version.js +6 -4
- package/dist/workers/pi-worker-docs.d.ts +18 -4
- package/dist/workers/pi-worker-docs.js +57 -19
- package/dist/workers/research-cache.d.ts +2 -13
- package/dist/workers/research-cache.js +22 -46
- package/dist/workers/shared.d.ts +16 -5
- package/dist/workers/shared.js +0 -0
- package/package.json +1 -1
|
@@ -277,8 +277,17 @@ async function runPlanCommand(ctx, cwd, planId, task, commandDeps) {
|
|
|
277
277
|
}
|
|
278
278
|
catch (err) {
|
|
279
279
|
const msg = err instanceof Error ? err.message : String(err);
|
|
280
|
+
// A user stop is not a fault, and the sentinel is not a sentence. The
|
|
281
|
+
// cancel checkpoint in runPlanningChild throws USER_CANCELLED here, so
|
|
282
|
+
// without this branch a /task-cancel during planning reads as a red
|
|
283
|
+
// "PLAN_0001 stopped — __user_cancelled__".
|
|
284
|
+
if (msg === USER_CANCELLED) {
|
|
285
|
+
await updateTaskFrontMatter(cwd, planId, { state: 'cancelled' }).catch(() => { });
|
|
286
|
+
announceTerminal(ctx, `${planId} cancelled.`, 'warning', { push: false });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
280
289
|
await updateTaskFrontMatter(cwd, planId, {
|
|
281
|
-
state:
|
|
290
|
+
state: 'failed',
|
|
282
291
|
reason: msg.slice(0, 200)
|
|
283
292
|
}).catch(() => { });
|
|
284
293
|
// No push: a plan is a conversation, not a task; the run it hands off
|
package/dist/task/prompts.d.ts
CHANGED
|
@@ -26,7 +26,13 @@ export declare const COMPRESS_LABEL_PROMPT: (title: string, maxChars: number) =>
|
|
|
26
26
|
declare const REFINE_PROMPT: (raw: string, planContext?: string, existingFiles?: string, contracts?: string, directives?: string) => string;
|
|
27
27
|
declare const RESEARCH_READ_ONLY_CONSTRAINT = "IMPORTANT: You are ONLY allowed to READ. Do NOT create, modify, or delete any files. Use the read, grep, find, and ls tools to inspect the repo.";
|
|
28
28
|
declare const RESEARCH_FILES_PROMPT: (refined: string) => string;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* `ecosystems` is what `detectEcosystems` found in the worker's cwd. It is REQUIRED,
|
|
31
|
+
* and an empty list is a real answer: a directory with no package manifest has no
|
|
32
|
+
* registry to look a library up in, and the paragraph below has to say so rather
|
|
33
|
+
* than inviting a lookup that will be refused.
|
|
34
|
+
*/
|
|
35
|
+
declare const RESEARCH_APIS_PROMPT: (refined: string, filesMap: string | undefined, ecosystems: readonly string[]) => string;
|
|
30
36
|
declare const RESEARCH_CONTEXT_PROMPT: (refined: string) => string;
|
|
31
37
|
declare const RESEARCH_TOOLING_PROMPT: (refined: string) => string;
|
|
32
38
|
declare const GRILL_GEN_PROMPT: (refined: string, research: string, priorQA: string) => string;
|
package/dist/task/prompts.js
CHANGED
|
@@ -97,13 +97,23 @@ No section header. No other sections. No preamble.
|
|
|
97
97
|
|
|
98
98
|
Task:
|
|
99
99
|
${refined}`;
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `ecosystems` is what `detectEcosystems` found in the worker's cwd. It is REQUIRED,
|
|
102
|
+
* and an empty list is a real answer: a directory with no package manifest has no
|
|
103
|
+
* registry to look a library up in, and the paragraph below has to say so rather
|
|
104
|
+
* than inviting a lookup that will be refused.
|
|
105
|
+
*/
|
|
106
|
+
const RESEARCH_APIS_PROMPT = (refined, filesMap, ecosystems) => `You are doing targeted research for an AI coding agent. Use the read, grep, find, and ls tools${ecosystems.length ? ' — and `pi-worker-docs` for installed packages —' : ''} to identify the commands, functions, types, and interfaces the agent will use for the following task.
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
${ecosystems.length ?
|
|
109
|
+
`LIBRARY PACKAGES — use pi-worker-docs, NOT file reads: for any third-party package, call \`pi-worker-docs(module, query)\` to get its type signatures and API surface. Do NOT open installed-package source files directly — those reads are expensive and produce far more noise than the tool. The tool returns a compact, focused excerpt in a fraction of the token cost. This project's package ecosystems are: ${ecosystems.join(', ')}.`
|
|
110
|
+
: 'LIBRARY PACKAGES — do NOT look them up: this directory holds no package manifest, so `pi-worker-docs` has no registry to read and will refuse. Do not list an external library API you cannot check here.'}
|
|
103
111
|
|
|
104
|
-
PROJECT SOURCE — use pi-worker-docs with module ".", NOT file reads: for any function, class, type, or interface defined in THIS project's own
|
|
112
|
+
PROJECT SOURCE — use pi-worker-docs with module ".", NOT file reads: for any function, class, type, or interface defined in THIS project's own source (e.g. "what does requireAuth check?", "what does CreateListingSchema look like?", "what does the listings query module export?"), call \`pi-worker-docs(".", query)\` instead of reading the file. The tool indexes all git-tracked source files and returns only the relevant chunks — far cheaper than reading whole files.
|
|
105
113
|
|
|
106
|
-
|
|
114
|
+
${ecosystems.includes('npm') ?
|
|
115
|
+
`RUNTIME BUILTINS — verify, do NOT echo: a task (or the spec doc it references) may name a runtime/builtin import like \`bun:sql\`, \`bun:sqlite\`, \`node:fs\`, or \`Bun.password\`. A runtime exposes only a small FIXED set of \`<runtime>:<submodule>\` modules, and a spec doc can confidently name one that does not exist. Before you list ANY \`<pkg>:<sub>\` specifier, confirm it with \`pi-worker-docs\` (e.g. \`pi-worker-docs("bun:sql", "sql tagged template and SQL class — the import")\` — the tool resolves the runtime's real types) and emit the CANONICAL import the types actually prove, NOT the string copied from the task. Concretely: Bun's SQL client is \`import { sql } from "bun"\` (or \`Bun.sql\` / \`new SQL()\`) — there is NO \`bun:sql\` module. Never pass an unverified colon-specifier through to the APIS list; a phantom import laundered here becomes fabricated \`declare module\` shims in the implementation.`
|
|
116
|
+
: ''}
|
|
107
117
|
|
|
108
118
|
APIS owns symbols and commands BY NAME ONLY. Do NOT include any file path or path fragment — no \`package.json\`, no \`./src/foo.ts\`, no \`package.json#scripts.lint\`. If the symbol is a script defined in package.json, write the invocation (\`npm run lint\`), not its location. If the symbol is a config file, it does not belong in APIS at all — it belongs in FILES.
|
|
109
119
|
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { classifyWorkerFailure } from '../workers/worker-failure.js';
|
|
22
22
|
import { classifyContextSilence, countBullets } from './context-silence.js';
|
|
23
|
+
import { cancelCheckpoint } from './cancel-points.js';
|
|
24
|
+
import { USER_CANCELLED } from './child-runner.js';
|
|
23
25
|
/**
|
|
24
26
|
* Task-file heading under which a research worker's validated output is cached.
|
|
25
27
|
* A resumed research phase reads these to skip workers that already succeeded,
|
|
@@ -403,5 +405,14 @@ export async function runResearchWorker(spec, run, prior = []) {
|
|
|
403
405
|
// a truncated section can still carry a laundered claim.
|
|
404
406
|
const sectionText = spec.postProcess ? spec.postProcess(rawText) : rawText;
|
|
405
407
|
await run.persistSection(cacheHeading, sectionText);
|
|
408
|
+
// SAFE CHECKPOINT: this worker's section is on disk and the cache read at the
|
|
409
|
+
// top of this function is what a resume uses to skip it, so stopping between
|
|
410
|
+
// workers repeats nothing. Research is the phase's long pole and the workers
|
|
411
|
+
// run serially, which is what makes this the one seam that costs nothing in
|
|
412
|
+
// either direction. USER_CANCELLED reuses the phase's existing cancel path.
|
|
413
|
+
if (cancelCheckpoint(`research:${spec.section}`)) {
|
|
414
|
+
run.logDebug?.(`cancel: stopping after research worker ${spec.section}`);
|
|
415
|
+
throw new Error(USER_CANCELLED);
|
|
416
|
+
}
|
|
406
417
|
return { name: spec.section, text: sectionText };
|
|
407
418
|
}
|
package/dist/task/run-bracket.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { armCancelListener, disarmCancelListener } from './cancel-input.js';
|
|
2
|
-
import { beginRun, endRun } from './mid-run-input.js';
|
|
2
|
+
import { beginRun, endRun, isRunActive } from './mid-run-input.js';
|
|
3
3
|
import { reportDroppedInput } from './dropped-input.js';
|
|
4
4
|
import { publishLifecycleNotice } from '../remote/bridge.js';
|
|
5
5
|
import { pushNotify } from '../remote/push.js';
|
|
6
|
+
import { resetCancel } from './cancel-points.js';
|
|
6
7
|
/**
|
|
7
8
|
* Run `fn` as the owner of the session: hold mid-run input and arm the terminal
|
|
8
9
|
* interception for exactly its duration, then release both — on return AND on
|
|
@@ -10,6 +11,15 @@ import { pushNotify } from '../remote/push.js';
|
|
|
10
11
|
* Nests: an inner bracket neither re-renders the surfaces nor un-arms the outer.
|
|
11
12
|
*/
|
|
12
13
|
export async function withRun(ctx, opts, fn) {
|
|
14
|
+
// A cancel is scoped to the run that was asked to stop. runAutoLoop already
|
|
15
|
+
// cleared the flag at both ends of its own loop; the bracket is what gives
|
|
16
|
+
// every OTHER entry point the same guarantee, so a /task-cancel raised on a
|
|
17
|
+
// bare /task cannot survive to stop the next run at its first phase.
|
|
18
|
+
// Outermost only: an inner bracket clearing it would drop the request the
|
|
19
|
+
// outer loop has not observed yet.
|
|
20
|
+
const outermost = !isRunActive();
|
|
21
|
+
if (outermost)
|
|
22
|
+
resetCancel();
|
|
13
23
|
beginRun();
|
|
14
24
|
armCancelListener(ctx, opts.onCancel);
|
|
15
25
|
try {
|
|
@@ -17,6 +27,8 @@ export async function withRun(ctx, opts, fn) {
|
|
|
17
27
|
}
|
|
18
28
|
finally {
|
|
19
29
|
disarmCancelListener();
|
|
30
|
+
if (outermost)
|
|
31
|
+
resetCancel();
|
|
20
32
|
reportDroppedInput(endRun(), ctx);
|
|
21
33
|
}
|
|
22
34
|
}
|
package/dist/task/task-gates.js
CHANGED
|
@@ -10,6 +10,8 @@ import { attributeEnforceFailure } from './enforce-attribution.js';
|
|
|
10
10
|
// re-check-side parser (extractDeletedDebtPath) have to move together.
|
|
11
11
|
import { crossTaskDeletionReason } from './accept-debt.js';
|
|
12
12
|
import { clampOutput } from './clamp-output.js';
|
|
13
|
+
import { cancelCheckpoint } from './cancel-points.js';
|
|
14
|
+
import { updateTaskFrontMatter } from './task-io.js';
|
|
13
15
|
/**
|
|
14
16
|
* How many times a verify FAIL may be auto-fixed UNATTENDED (the research
|
|
15
17
|
* recommended AUTOFIX, so pi re-runs the impl turn without prompting) before the
|
|
@@ -114,6 +116,24 @@ export async function resolveVerifyGate(ctxIn, deps, p, rec, routeRootCause) {
|
|
|
114
116
|
// YOLO only: has the one-attempt rescue below already been spent on this task?
|
|
115
117
|
let yoloRescueUsed = false;
|
|
116
118
|
while (!verified.ok) {
|
|
119
|
+
// SAFE CHECKPOINT (before a resolution round): a round is a bounded
|
|
120
|
+
// lint fix, a research child and possibly a whole implementation
|
|
121
|
+
// re-run, so a cancel observed only INSIDE one buys all of that
|
|
122
|
+
// first. At the TOP of the loop because every later position is past
|
|
123
|
+
// something that already recorded itself — the unattended branch
|
|
124
|
+
// increments the counter, writes its `## gates` line and toasts
|
|
125
|
+
// "auto-fixing…" before it reaches any code below.
|
|
126
|
+
//
|
|
127
|
+
// The task file still reads `completed` here, written at spec handoff
|
|
128
|
+
// before any of this ran, and `completed` is not in RESUMABLE_STATES.
|
|
129
|
+
// Returning `cancelled` without this write would leave the announced
|
|
130
|
+
// "resume with /task-resume" pointing at a file /task-resume skips —
|
|
131
|
+
// the work is unverified AND uncommitted at this point, so that is the
|
|
132
|
+
// one place in the gate where a missed demotion loses it.
|
|
133
|
+
if (cancelCheckpoint('gate:pre-resolution')) {
|
|
134
|
+
await updateTaskFrontMatter(p.cwd, p.taskId, { state: 'cancelled' }).catch(() => { });
|
|
135
|
+
return { stop: { kind: 'cancelled', ctx: active } };
|
|
136
|
+
}
|
|
117
137
|
const failReason = verified.reason ?? 'did not verify';
|
|
118
138
|
// GRADUATED resolution: a repo-health FAIL (pure static findings) gets ONE
|
|
119
139
|
// bounded fix attempt before the picker — smallest tool first. Applied →
|
|
@@ -671,6 +691,20 @@ export async function runGatesForTask(ctxIn, deps, p) {
|
|
|
671
691
|
else
|
|
672
692
|
notifyBoth(active, line, 'warning');
|
|
673
693
|
}
|
|
694
|
+
// SAFE CHECKPOINT (post task commit): the work verified, the parent entry is
|
|
695
|
+
// checked off and the snapshot is in HEAD. Only the enforce pass is skipped,
|
|
696
|
+
// and enforce is re-runnable.
|
|
697
|
+
//
|
|
698
|
+
// Returns `done`, not `cancelled`: this task IS done, and `cancelled` would
|
|
699
|
+
// announce "resume with /task-resume" over a task whose work is already
|
|
700
|
+
// committed — a resume there re-runs the whole spec pipeline to redo it. The
|
|
701
|
+
// flag stays raised, so /task-auto stops one step later at loop-top with the
|
|
702
|
+
// right wording and a ticked checkbox, and a bare /task simply ends. The trail
|
|
703
|
+
// line is what keeps the skip from being silent.
|
|
704
|
+
if (cancelCheckpoint('gate:post-commit')) {
|
|
705
|
+
await rec('enforce: skipped — cancel requested after the task snapshot committed');
|
|
706
|
+
return { kind: 'done', ctx: active };
|
|
707
|
+
}
|
|
674
708
|
await runEnforcePass(active, deps, p, rec, routeRootCause, { cleanPass, commit });
|
|
675
709
|
return { kind: 'done', ctx: active };
|
|
676
710
|
}
|
|
@@ -2,24 +2,37 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
|
+
/**
|
|
6
|
+
* Bump whenever the shape below changes in a way old rows cannot satisfy. The
|
|
7
|
+
* migration is a DROP and rebuild: this is a derived cache of package sources,
|
|
8
|
+
* so re-indexing costs a walk, and hand-written ALTERs cost a defect class.
|
|
9
|
+
*/
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* `ecosystem` scopes every row to the registry it came from, so `text` on npm and
|
|
13
|
+
* `text` on Hackage are different packages. Project-source rows use their own
|
|
14
|
+
* scope value rather than a registry id.
|
|
15
|
+
*/
|
|
5
16
|
const SCHEMA_SQL = `
|
|
6
17
|
CREATE TABLE IF NOT EXISTS packages (
|
|
18
|
+
ecosystem TEXT NOT NULL DEFAULT 'npm',
|
|
7
19
|
name TEXT NOT NULL,
|
|
8
20
|
version TEXT NOT NULL,
|
|
9
21
|
content_hash TEXT NOT NULL,
|
|
10
22
|
indexed_at INTEGER NOT NULL,
|
|
11
|
-
PRIMARY KEY (name, version)
|
|
23
|
+
PRIMARY KEY (ecosystem, name, version)
|
|
12
24
|
);
|
|
13
25
|
|
|
14
26
|
CREATE TABLE IF NOT EXISTS chunks (
|
|
15
27
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
ecosystem TEXT NOT NULL DEFAULT 'npm',
|
|
16
29
|
name TEXT NOT NULL,
|
|
17
30
|
version TEXT NOT NULL,
|
|
18
31
|
file_path TEXT NOT NULL,
|
|
19
32
|
kind TEXT NOT NULL CHECK (kind IN ('dts','readme')),
|
|
20
33
|
content TEXT NOT NULL
|
|
21
34
|
);
|
|
22
|
-
CREATE INDEX IF NOT EXISTS chunks_pkg ON chunks(name, version);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS chunks_pkg ON chunks(ecosystem, name, version);
|
|
23
36
|
|
|
24
37
|
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
25
38
|
content,
|
|
@@ -34,6 +47,14 @@ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
|
|
|
34
47
|
INSERT INTO chunks_fts(chunks_fts, rowid, content) VALUES('delete', old.id, old.content);
|
|
35
48
|
END;
|
|
36
49
|
`;
|
|
50
|
+
// Order matters: the triggers and the FTS index reference `chunks`.
|
|
51
|
+
const DROP_SQL = `
|
|
52
|
+
DROP TRIGGER IF EXISTS chunks_ai;
|
|
53
|
+
DROP TRIGGER IF EXISTS chunks_ad;
|
|
54
|
+
DROP TABLE IF EXISTS chunks_fts;
|
|
55
|
+
DROP TABLE IF EXISTS chunks;
|
|
56
|
+
DROP TABLE IF EXISTS packages;
|
|
57
|
+
`;
|
|
37
58
|
const req = createRequire(import.meta.url);
|
|
38
59
|
// Branch on the RUNTIME, not on a try/catch: each builtin exists in exactly one
|
|
39
60
|
// of them. Requiring `bun:sqlite` under node throws MODULE_NOT_FOUND, and
|
|
@@ -61,9 +82,35 @@ export function openCache(dbPath) {
|
|
|
61
82
|
db.exec('PRAGMA journal_mode = WAL;');
|
|
62
83
|
db.exec('PRAGMA synchronous = NORMAL;');
|
|
63
84
|
db.exec('PRAGMA foreign_keys = ON;');
|
|
64
|
-
|
|
85
|
+
// A blocking busy handler, not a tuned delay: research children open this
|
|
86
|
+
// cache concurrently, and the loser of the migration lock has to WAIT for the
|
|
87
|
+
// winner's COMMIT rather than throw SQLITE_BUSY on the spot. The bound only
|
|
88
|
+
// stops a wedged handle hanging the worker forever.
|
|
89
|
+
db.exec('PRAGMA busy_timeout = 30000;');
|
|
90
|
+
migrate(db);
|
|
65
91
|
return {
|
|
66
92
|
db,
|
|
67
93
|
close: () => db.close()
|
|
68
94
|
};
|
|
69
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* `CREATE TABLE IF NOT EXISTS` is a no-op against an older shape, so the version
|
|
98
|
+
* check has to run BEFORE it or a stale table survives untouched.
|
|
99
|
+
*/
|
|
100
|
+
function migrate(db) {
|
|
101
|
+
db.exec('BEGIN IMMEDIATE');
|
|
102
|
+
try {
|
|
103
|
+
const row = db.prepare('PRAGMA user_version').get();
|
|
104
|
+
// A brand-new database also reads 0, hence DROP ... IF EXISTS.
|
|
105
|
+
if ((row?.user_version ?? 0) < SCHEMA_VERSION) {
|
|
106
|
+
db.exec(DROP_SQL);
|
|
107
|
+
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
|
|
108
|
+
}
|
|
109
|
+
db.exec(SCHEMA_SQL);
|
|
110
|
+
db.exec('COMMIT');
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
db.exec('ROLLBACK');
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -53,15 +53,18 @@ export declare function splitAtMatches(text: string, re: RegExp): string[];
|
|
|
53
53
|
*/
|
|
54
54
|
export declare function sliceBytes(s: string, maxBytes: number): string[];
|
|
55
55
|
/**
|
|
56
|
-
* Chunk a declaration file
|
|
57
|
-
*
|
|
56
|
+
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
57
|
+
* file it came from.
|
|
58
58
|
*
|
|
59
59
|
* `relPath` is a MODEL-FACING label and is used exactly as given. docs-index.ts
|
|
60
60
|
* normalises it to POSIX (`.replace(/\\/g, '/')`) so a package index is identical
|
|
61
61
|
* across platforms; docs-project.ts passes `path.relative` through with the native
|
|
62
62
|
* separator. It is never re-joined to the filesystem, so neither is wrong — this
|
|
63
63
|
* leaves the choice with the caller that has a reason for it.
|
|
64
|
+
*
|
|
65
|
+
* `splitRe` and `commentPrefix` default to the TypeScript pair, which is what
|
|
66
|
+
* both the project corpus and npm packages are written in.
|
|
64
67
|
*/
|
|
65
|
-
export declare function chunkDeclarations(content: string, relPath: string): string[];
|
|
68
|
+
export declare function chunkDeclarations(content: string, relPath: string, splitRe?: RegExp, commentPrefix?: string): string[];
|
|
66
69
|
/** Chunk a README, one chunk per top-level section, each labelled by heading. */
|
|
67
70
|
export declare function chunkReadme(content: string): string[];
|
|
@@ -90,22 +90,25 @@ export function sliceBytes(s, maxBytes) {
|
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Chunk a declaration file
|
|
94
|
-
*
|
|
93
|
+
* Chunk a declaration file, one chunk per declaration, each labelled with the
|
|
94
|
+
* file it came from.
|
|
95
95
|
*
|
|
96
96
|
* `relPath` is a MODEL-FACING label and is used exactly as given. docs-index.ts
|
|
97
97
|
* normalises it to POSIX (`.replace(/\\/g, '/')`) so a package index is identical
|
|
98
98
|
* across platforms; docs-project.ts passes `path.relative` through with the native
|
|
99
99
|
* separator. It is never re-joined to the filesystem, so neither is wrong — this
|
|
100
100
|
* leaves the choice with the caller that has a reason for it.
|
|
101
|
+
*
|
|
102
|
+
* `splitRe` and `commentPrefix` default to the TypeScript pair, which is what
|
|
103
|
+
* both the project corpus and npm packages are written in.
|
|
101
104
|
*/
|
|
102
|
-
export function chunkDeclarations(content, relPath) {
|
|
105
|
+
export function chunkDeclarations(content, relPath, splitRe = DECL_SPLIT_RE, commentPrefix = '//') {
|
|
103
106
|
const chunks = [];
|
|
104
|
-
for (const part of splitAtMatches(content, new RegExp(
|
|
107
|
+
for (const part of splitAtMatches(content, new RegExp(splitRe.source, 'gm'))) {
|
|
105
108
|
const trimmed = part.trim();
|
|
106
109
|
if (!trimmed)
|
|
107
110
|
continue;
|
|
108
|
-
const prefixed =
|
|
111
|
+
const prefixed = `${commentPrefix} ${relPath}\n${trimmed}`;
|
|
109
112
|
if (Buffer.byteLength(prefixed, 'utf8') > MAX_CHUNK_BYTES) {
|
|
110
113
|
for (const slice of sliceBytes(prefixed, MAX_CHUNK_BYTES))
|
|
111
114
|
chunks.push(slice);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { openCache as defaultOpenCache } from './docs-cache.js';
|
|
2
2
|
import { ensureIndexed as defaultEnsureIndexed } from './docs-index.js';
|
|
3
|
+
import { type EcosystemId, type EcosystemIo, type EcosystemProfile } from './docs-ecosystems.js';
|
|
3
4
|
import { resolvePackage as defaultResolvePackage, type ResolvedPackage } from './docs-resolve.js';
|
|
4
5
|
import { retrieveChunks as defaultRetrieveChunks, type RetrievedChunk } from './docs-retrieve.js';
|
|
5
6
|
import { npmVersionLookup as defaultNpmVersionLookup, type NpmVersionInfo } from './npm-version.js';
|
|
@@ -37,6 +38,8 @@ export type DocsRawResult = {
|
|
|
37
38
|
autoInstalled?: boolean;
|
|
38
39
|
autoInstallPin?: AutoInstallPin;
|
|
39
40
|
npmVersion?: NpmVersionInfo | null;
|
|
41
|
+
/** The registry the answer came from, for the block that leads it. */
|
|
42
|
+
registryLabel?: string;
|
|
40
43
|
} | {
|
|
41
44
|
kind: 'no_chunks';
|
|
42
45
|
pkg: ResolvedPackage;
|
|
@@ -46,10 +49,12 @@ export type DocsRawResult = {
|
|
|
46
49
|
autoInstalled?: boolean;
|
|
47
50
|
autoInstallPin?: AutoInstallPin;
|
|
48
51
|
npmVersion?: NpmVersionInfo | null;
|
|
52
|
+
/** The registry the answer came from, for the block that leads it. */
|
|
53
|
+
registryLabel?: string;
|
|
49
54
|
} | {
|
|
50
55
|
kind: 'error';
|
|
51
56
|
message: string;
|
|
52
|
-
resolveError?: 'not_installed' | 'invalid_name';
|
|
57
|
+
resolveError?: 'not_installed' | 'invalid_name' | 'unsupported_ecosystem' | 'ambiguous_ecosystem';
|
|
53
58
|
installError?: string;
|
|
54
59
|
version?: string;
|
|
55
60
|
hitCache?: boolean;
|
|
@@ -57,11 +62,15 @@ export type DocsRawResult = {
|
|
|
57
62
|
autoInstalled?: boolean;
|
|
58
63
|
autoInstallPin?: AutoInstallPin;
|
|
59
64
|
npmVersion?: NpmVersionInfo | null;
|
|
65
|
+
/** The registry the answer came from, for the block that leads it. */
|
|
66
|
+
registryLabel?: string;
|
|
60
67
|
};
|
|
61
68
|
export interface DocsRawInput {
|
|
62
69
|
pkg: string;
|
|
63
70
|
query: string;
|
|
64
71
|
cwd: string;
|
|
72
|
+
/** Which registry to read, for a repo holding more than one manifest. */
|
|
73
|
+
ecosystem?: EcosystemId;
|
|
65
74
|
autoInstall?: boolean;
|
|
66
75
|
resolvePackage?: typeof defaultResolvePackage;
|
|
67
76
|
ensureIndexed?: typeof defaultEnsureIndexed;
|
|
@@ -69,6 +78,8 @@ export interface DocsRawInput {
|
|
|
69
78
|
openCache?: typeof defaultOpenCache;
|
|
70
79
|
spawn?: SpawnFn;
|
|
71
80
|
npmVersionLookup?: typeof defaultNpmVersionLookup;
|
|
81
|
+
/** Overrides for the filesystem and network a non-npm row reaches through. */
|
|
82
|
+
io?: Partial<EcosystemIo>;
|
|
72
83
|
signal?: AbortSignal;
|
|
73
84
|
}
|
|
74
85
|
export interface DocsFocusedResult {
|
|
@@ -162,7 +173,7 @@ export declare function findDeclaredRange(parentPkg: string, cwd: string): strin
|
|
|
162
173
|
* `bun`, and cannot declare `bun-types`, so a sentence about what package.json
|
|
163
174
|
* does or does not say has to be a sentence about `bun`.
|
|
164
175
|
*/
|
|
165
|
-
export declare function buildVersionBanner(pin: AutoInstallPin | undefined, resolved: string, version: string, cwd: string): string;
|
|
176
|
+
export declare function buildVersionBanner(pin: AutoInstallPin | undefined, resolved: string, version: string, cwd: string, profile?: EcosystemProfile): string;
|
|
166
177
|
export declare function getDocsModulesDir(): string;
|
|
167
178
|
export declare function ensureDocsModulesDir(dir: string): void;
|
|
168
179
|
/**
|
|
@@ -207,8 +218,13 @@ export interface AcquireInput {
|
|
|
207
218
|
name: string;
|
|
208
219
|
cwd: string;
|
|
209
220
|
spawn: SpawnFn;
|
|
210
|
-
|
|
221
|
+
/** Overrides `profile.resolve` when given, for callers that inject a resolver. */
|
|
222
|
+
resolvePackage?: typeof defaultResolvePackage;
|
|
211
223
|
signal: AbortSignal | undefined;
|
|
224
|
+
/** Which registry to acquire from. */
|
|
225
|
+
profile?: EcosystemProfile;
|
|
226
|
+
/** The row's filesystem and network reach. Built from `spawn` when absent. */
|
|
227
|
+
io?: EcosystemIo;
|
|
212
228
|
}
|
|
213
229
|
/**
|
|
214
230
|
* Get a package onto disk and resolved: resolve from `cwd`, and on
|
|
@@ -231,6 +247,14 @@ export interface AcquireInput {
|
|
|
231
247
|
* sites should differ only about WHETHER to install, never about HOW.
|
|
232
248
|
*/
|
|
233
249
|
export declare function acquirePackage(input: AcquireInput): Promise<AcquireOutcome>;
|
|
250
|
+
/** The docs pipeline's adapter over the shared redirect walk (docs-resolve.ts):
|
|
251
|
+
* hops resolve through the auto-installing lookup, so a declaration package that is
|
|
252
|
+
* declared but not yet on disk is fetched rather than abandoned. */
|
|
253
|
+
export declare function resolveTypeSourceForDocs(pkg: ResolvedPackage, requested: string, cwd: string, spawn: SpawnFn, resolvePackage: typeof defaultResolvePackage, signal: AbortSignal | undefined): Promise<{
|
|
254
|
+
pkg: ResolvedPackage;
|
|
255
|
+
installed: boolean;
|
|
256
|
+
pin?: AutoInstallPin;
|
|
257
|
+
}>;
|
|
234
258
|
export declare function docsRaw(input: DocsRawInput): Promise<DocsRawResult>;
|
|
235
259
|
export declare function docsFocused(input: DocsFocusedInput): Promise<DocsFocusedResult>;
|
|
236
260
|
export declare function buildPrompt(pkg: ResolvedPackage, query: string, content: string): string;
|