@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/dist/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
package/dist/config/config.d.ts
CHANGED
|
@@ -189,5 +189,31 @@ export declare const DEFAULT_CONFIG: PiTaskConfig;
|
|
|
189
189
|
* object/number can't reach the child argv as `-e [object Object]`.
|
|
190
190
|
*/
|
|
191
191
|
export declare function sanitizeExtensionWhitelist(value: unknown): string[];
|
|
192
|
+
/**
|
|
193
|
+
* How each setting's STORED value becomes a safe in-memory value — one loader per
|
|
194
|
+
* key, keyed on the config's own type.
|
|
195
|
+
*
|
|
196
|
+
* `ConfigItem` (config/register.ts) already absorbed three of the four edits its
|
|
197
|
+
* own header names: *"Adding one enum setting meant four coordinated edits (row,
|
|
198
|
+
* format arm, parse arm, sanitizer) and NONE of them failed to compile if you
|
|
199
|
+
* forgot it."* The sanitizer was the fourth, and it stayed behind in this file as
|
|
200
|
+
* a hand-ordered statement ladder covering 7 of 14 keys. This is the mapped type
|
|
201
|
+
* that closes it: a new field on `PiTaskConfig` is a compile error until it
|
|
202
|
+
* declares how a hostile value becomes a safe one.
|
|
203
|
+
*/
|
|
204
|
+
export declare const CONFIG_LOADERS: {
|
|
205
|
+
[K in keyof PiTaskConfig]: (raw: unknown) => PiTaskConfig[K];
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Turn parsed config JSON into a `PiTaskConfig`. Pure — this is the seam the load
|
|
209
|
+
* block used to have none of, so every hostile-value case was reachable only
|
|
210
|
+
* through `getConfig()`, which reads the developer's own machine.
|
|
211
|
+
*
|
|
212
|
+
* A non-object `raw` yields the defaults. That is not merely defensive: the old
|
|
213
|
+
* block spread `parsed` directly, and `{...DEFAULT_CONFIG, ...'ab'}` produces
|
|
214
|
+
* numeric index keys. Keys absent from the table are dropped rather than carried
|
|
215
|
+
* into the config object.
|
|
216
|
+
*/
|
|
217
|
+
export declare function loadConfig(raw: unknown): PiTaskConfig;
|
|
192
218
|
export declare function getConfig(): PiTaskConfig;
|
|
193
219
|
export declare function saveConfig(config: PiTaskConfig): Promise<void>;
|
package/dist/config/config.js
CHANGED
|
@@ -115,6 +115,73 @@ export function sanitizeExtensionWhitelist(value) {
|
|
|
115
115
|
return [];
|
|
116
116
|
return value.filter((p) => typeof p === 'string' && p.trim().length > 0);
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* A boolean setting's loader. Only a REAL boolean counts; anything else falls
|
|
120
|
+
* back to the shipped default.
|
|
121
|
+
*
|
|
122
|
+
* This is `yoloMode`'s guard, generalised. Its comment — *"a hand-edited
|
|
123
|
+
* `"yoloMode": "false"` is a truthy string"* — was true verbatim of the other
|
|
124
|
+
* seven booleans, none of which had it, so a stale `"verifyWork": "off"` reached
|
|
125
|
+
* `getConfig().verifyWork` as truthy and a `"autoCommit": 0` reached it as falsy.
|
|
126
|
+
*/
|
|
127
|
+
function asBoolean(key) {
|
|
128
|
+
return raw => (typeof raw === 'boolean' ? raw : DEFAULT_CONFIG[key]);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* How each setting's STORED value becomes a safe in-memory value — one loader per
|
|
132
|
+
* key, keyed on the config's own type.
|
|
133
|
+
*
|
|
134
|
+
* `ConfigItem` (config/register.ts) already absorbed three of the four edits its
|
|
135
|
+
* own header names: *"Adding one enum setting meant four coordinated edits (row,
|
|
136
|
+
* format arm, parse arm, sanitizer) and NONE of them failed to compile if you
|
|
137
|
+
* forgot it."* The sanitizer was the fourth, and it stayed behind in this file as
|
|
138
|
+
* a hand-ordered statement ladder covering 7 of 14 keys. This is the mapped type
|
|
139
|
+
* that closes it: a new field on `PiTaskConfig` is a compile error until it
|
|
140
|
+
* declares how a hostile value becomes a safe one.
|
|
141
|
+
*/
|
|
142
|
+
export const CONFIG_LOADERS = {
|
|
143
|
+
remote: asBoolean('remote'),
|
|
144
|
+
autoCommit: asBoolean('autoCommit'),
|
|
145
|
+
orientation: asBoolean('orientation'),
|
|
146
|
+
enforceGuidelines: asBoolean('enforceGuidelines'),
|
|
147
|
+
verifyWork: asBoolean('verifyWork'),
|
|
148
|
+
parallelResearchWorkers: asBoolean('parallelResearchWorkers'),
|
|
149
|
+
researchCache: asBoolean('researchCache'),
|
|
150
|
+
yoloMode: asBoolean('yoloMode'),
|
|
151
|
+
// A hand-edited or stale enum value must not leak an unknown provider into
|
|
152
|
+
// the dispatch switch — fall back to the default.
|
|
153
|
+
searchProvider: raw => (isSearchProvider(raw) ? raw : DEFAULT_CONFIG.searchProvider),
|
|
154
|
+
extensionWhitelist: sanitizeExtensionWhitelist,
|
|
155
|
+
requestTimeoutMs: sanitizeRequestTimeoutMs,
|
|
156
|
+
commandTimeoutExemptTools: sanitizeCommandTimeoutExemptTools,
|
|
157
|
+
streamInactivityMs: sanitizeStreamInactivityMs,
|
|
158
|
+
debugLogs: sanitizeDebugLogs
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Turn parsed config JSON into a `PiTaskConfig`. Pure — this is the seam the load
|
|
162
|
+
* block used to have none of, so every hostile-value case was reachable only
|
|
163
|
+
* through `getConfig()`, which reads the developer's own machine.
|
|
164
|
+
*
|
|
165
|
+
* A non-object `raw` yields the defaults. That is not merely defensive: the old
|
|
166
|
+
* block spread `parsed` directly, and `{...DEFAULT_CONFIG, ...'ab'}` produces
|
|
167
|
+
* numeric index keys. Keys absent from the table are dropped rather than carried
|
|
168
|
+
* into the config object.
|
|
169
|
+
*/
|
|
170
|
+
export function loadConfig(raw) {
|
|
171
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
172
|
+
return { ...DEFAULT_CONFIG };
|
|
173
|
+
}
|
|
174
|
+
const stored = raw;
|
|
175
|
+
const out = { ...DEFAULT_CONFIG };
|
|
176
|
+
for (const key of Object.keys(CONFIG_LOADERS)) {
|
|
177
|
+
// `undefined` reaches the loader like any other hostile value: every
|
|
178
|
+
// loader answers a missing key with its own default, which is what the
|
|
179
|
+
// old `delete parsed.X` + spread did.
|
|
180
|
+
;
|
|
181
|
+
out[key] = CONFIG_LOADERS[key](stored[key]);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
118
185
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
119
186
|
const _g = globalThis;
|
|
120
187
|
if (!_g.__piTaskConfig) {
|
|
@@ -125,23 +192,7 @@ const G = _g.__piTaskConfig;
|
|
|
125
192
|
// before any session_start handler fires.
|
|
126
193
|
if (!G.loaded) {
|
|
127
194
|
try {
|
|
128
|
-
|
|
129
|
-
const parsed = JSON.parse(raw);
|
|
130
|
-
// A hand-edited or stale enum value must not leak an unknown provider
|
|
131
|
-
// into the dispatch switch — fall back to the default.
|
|
132
|
-
if (!isSearchProvider(parsed.searchProvider))
|
|
133
|
-
delete parsed.searchProvider;
|
|
134
|
-
parsed.extensionWhitelist = sanitizeExtensionWhitelist(parsed.extensionWhitelist);
|
|
135
|
-
parsed.requestTimeoutMs = sanitizeRequestTimeoutMs(parsed.requestTimeoutMs);
|
|
136
|
-
parsed.commandTimeoutExemptTools = sanitizeCommandTimeoutExemptTools(parsed.commandTimeoutExemptTools);
|
|
137
|
-
parsed.streamInactivityMs = sanitizeStreamInactivityMs(parsed.streamInactivityMs);
|
|
138
|
-
// A hand-edited `"yoloMode": "false"` is a truthy string — it must not
|
|
139
|
-
// silently switch a watched run into unattended auto-pick. Only a real
|
|
140
|
-
// boolean counts; anything else falls back to the OFF default.
|
|
141
|
-
if (typeof parsed.yoloMode !== 'boolean')
|
|
142
|
-
delete parsed.yoloMode;
|
|
143
|
-
parsed.debugLogs = sanitizeDebugLogs(parsed.debugLogs);
|
|
144
|
-
G.config = { ...DEFAULT_CONFIG, ...parsed };
|
|
195
|
+
G.config = loadConfig(JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')));
|
|
145
196
|
}
|
|
146
197
|
catch {
|
|
147
198
|
G.config = { ...DEFAULT_CONFIG };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn as defaultSpawn, spawnSync as spawnSyncDefault } from 'node:child_process';
|
|
2
2
|
import { realStreamTimerDeps, StreamWatchdog } from './stream-watchdog.js';
|
|
3
|
+
import { workerChannel } from '../workers/worker-channels.js';
|
|
3
4
|
/** Grace period between SIGTERM and SIGKILL (ms). */
|
|
4
5
|
export const KILL_GRACE_MS = 5000;
|
|
5
6
|
/** Base flags shared by all child pi invocations. */
|
|
@@ -460,25 +461,17 @@ export function summarizeToolArgs(toolName, args) {
|
|
|
460
461
|
if (!args || typeof args !== 'object')
|
|
461
462
|
return '';
|
|
462
463
|
const a = args;
|
|
463
|
-
const clip = (s) => {
|
|
464
|
-
const one = s.replace(/\s+/g, ' ').trim();
|
|
465
|
-
return one.length > 60 ? one.slice(0, 59) + '…' : one;
|
|
466
|
-
};
|
|
467
464
|
if (toolName === 'bash' && typeof a.command === 'string') {
|
|
468
465
|
return a.command.replace(/\s+/g, ' ').trim();
|
|
469
466
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return `"${clip(a.query)}"`;
|
|
479
|
-
}
|
|
480
|
-
if (toolName === 'pi-worker-fetch' && typeof a.url === 'string') {
|
|
481
|
-
return clip(a.url);
|
|
467
|
+
// Each worker tool's argument shape is its own row's fact (worker-channels.ts);
|
|
468
|
+
// this used to be a third copy of the names AND a re-statement of each one's
|
|
469
|
+
// parameters. A row that has nothing to say falls through to the generic keys.
|
|
470
|
+
const channel = workerChannel(toolName);
|
|
471
|
+
if (channel) {
|
|
472
|
+
const summary = channel.summarize(a);
|
|
473
|
+
if (summary)
|
|
474
|
+
return summary;
|
|
482
475
|
}
|
|
483
476
|
if (typeof a.file_path === 'string')
|
|
484
477
|
return a.file_path;
|
|
@@ -190,12 +190,12 @@ export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
|
190
190
|
* caller is expected to have made `pass` mean "ran, exited 0, and changed
|
|
191
191
|
* nothing tracked" (`inv-no-write`).
|
|
192
192
|
*/
|
|
193
|
-
rerunVerify?: (command: string, debt: AcceptDebt) => VerifyRerunResult
|
|
194
|
-
}): {
|
|
193
|
+
rerunVerify?: (command: string, debt: AcceptDebt) => Promise<VerifyRerunResult>;
|
|
194
|
+
}): Promise<{
|
|
195
195
|
open: AcceptDebt[];
|
|
196
196
|
resolved: AcceptDebt[];
|
|
197
197
|
trail: string[];
|
|
198
|
-
}
|
|
198
|
+
}>;
|
|
199
199
|
/**
|
|
200
200
|
* Extract the paths whose EXISTENCE the reason asserts as the failure — a path
|
|
201
201
|
* token immediately followed by "exists" / "still exists", or by "must/should not
|
|
@@ -247,7 +247,9 @@ export declare function describeDebt(d: AcceptDebt): string;
|
|
|
247
247
|
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
248
248
|
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
249
249
|
*/
|
|
250
|
-
export declare function deriveOpenDebts(cwd: string, staticOk: boolean
|
|
250
|
+
export declare function deriveOpenDebts(cwd: string, staticOk: boolean,
|
|
251
|
+
/** The spawner for the VERIFY-COMMAND re-runs. Defaults to the real one. */
|
|
252
|
+
run?: CommandRunner, signal?: AbortSignal): Promise<{
|
|
251
253
|
openDebts: AcceptDebt[];
|
|
252
254
|
debtNote?: string;
|
|
253
255
|
trail?: string[];
|
|
@@ -272,4 +274,4 @@ export declare function deriveOpenDebts(cwd: string, staticOk: boolean): Promise
|
|
|
272
274
|
export declare function rerunDebtVerifyCommand(cwd: string, command: string,
|
|
273
275
|
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
274
276
|
* the guard's four outcomes are testable without a repo or a real command. */
|
|
275
|
-
run?: CommandRunner): VerifyRerunResult
|
|
277
|
+
run?: CommandRunner, signal?: AbortSignal): Promise<VerifyRerunResult>;
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -29,6 +29,7 @@ import { existsSync } from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import * as fsp from 'node:fs/promises';
|
|
31
31
|
import { runVerifyCommandLine, spawnCommand } from './command-run.js';
|
|
32
|
+
import { failClassOfReason, isStaticClass } from './verify-work.js';
|
|
32
33
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
33
34
|
import { makeLedger } from './ledger.js';
|
|
34
35
|
import { parseVerifyBlockStrict } from './spec-validation.js';
|
|
@@ -233,7 +234,7 @@ export async function writeAcceptDebts(cwd, debts) {
|
|
|
233
234
|
* behavioral and cannot be proven resolved without re-running the model.
|
|
234
235
|
*/
|
|
235
236
|
export function isStaticClassDebt(reason) {
|
|
236
|
-
return
|
|
237
|
+
return isStaticClass(failClassOfReason(reason));
|
|
237
238
|
}
|
|
238
239
|
/**
|
|
239
240
|
* The VERIFY-COMMAND class (nexttask 5): the ONE command a recorded reason itself
|
|
@@ -339,7 +340,7 @@ const MAX_VERIFY_RERUNS = 3;
|
|
|
339
340
|
* passed. FP-safe: the only auto-closes are ones a deterministic check can stand
|
|
340
341
|
* behind, and here the check is the task spec's own command.
|
|
341
342
|
*/
|
|
342
|
-
export function recheckAcceptDebts(debts, opts) {
|
|
343
|
+
export async function recheckAcceptDebts(debts, opts) {
|
|
343
344
|
const open = [];
|
|
344
345
|
const resolved = [];
|
|
345
346
|
const trail = [];
|
|
@@ -381,7 +382,7 @@ export function recheckAcceptDebts(debts, opts) {
|
|
|
381
382
|
rerunsLeft -= 1;
|
|
382
383
|
let r;
|
|
383
384
|
try {
|
|
384
|
-
r = opts.rerunVerify(cmd, d);
|
|
385
|
+
r = await opts.rerunVerify(cmd, d);
|
|
385
386
|
}
|
|
386
387
|
catch {
|
|
387
388
|
// A harness fault observes nothing, so it proves nothing.
|
|
@@ -499,8 +500,10 @@ export function describeDebt(d) {
|
|
|
499
500
|
* thing that can auto-close a static-class debt — so a caller that does not know
|
|
500
501
|
* must pass `false` (unprovable ⇒ stays open), never a guess.
|
|
501
502
|
*/
|
|
502
|
-
export async function deriveOpenDebts(cwd, staticOk
|
|
503
|
-
|
|
503
|
+
export async function deriveOpenDebts(cwd, staticOk,
|
|
504
|
+
/** The spawner for the VERIFY-COMMAND re-runs. Defaults to the real one. */
|
|
505
|
+
run = spawnCommand, signal) {
|
|
506
|
+
const { open: openRaw, resolved, trail } = await recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
504
507
|
staticOk,
|
|
505
508
|
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
506
509
|
// tree — a deterministic existence check, corroborating the per-file
|
|
@@ -509,7 +512,7 @@ export async function deriveOpenDebts(cwd, staticOk) {
|
|
|
509
512
|
// VERIFY-COMMAND class (nexttask 5): a debt that NAMES a command is settled
|
|
510
513
|
// by running that command, under the gate's own env-gap contract and behind
|
|
511
514
|
// the no-write guard below.
|
|
512
|
-
rerunVerify: cmd => rerunDebtVerifyCommand(cwd, cmd)
|
|
515
|
+
rerunVerify: cmd => rerunDebtVerifyCommand(cwd, cmd, run, signal)
|
|
513
516
|
});
|
|
514
517
|
if (resolved.length > 0)
|
|
515
518
|
await writeAcceptDebts(cwd, openRaw);
|
|
@@ -552,26 +555,27 @@ const DEBT_INFRA_GAP_RE = /ERR_POSTGRES_CONNECTION_CLOSED|ERR_MYSQL_CONNECTION|E
|
|
|
552
555
|
* the guard: the re-run is INCONCLUSIVE there, because "nothing changed" would be an
|
|
553
556
|
* assumption rather than an observation.
|
|
554
557
|
*/
|
|
555
|
-
export function rerunDebtVerifyCommand(cwd, command,
|
|
558
|
+
export async function rerunDebtVerifyCommand(cwd, command,
|
|
556
559
|
/** The spawner, for BOTH the command and the tracked-state reads. Injected so
|
|
557
560
|
* the guard's four outcomes are testable without a repo or a real command. */
|
|
558
|
-
run = spawnCommand) {
|
|
559
|
-
const tracked = () => {
|
|
560
|
-
const r = run({
|
|
561
|
+
run = spawnCommand, signal) {
|
|
562
|
+
const tracked = async () => {
|
|
563
|
+
const r = await run({
|
|
561
564
|
cwd,
|
|
562
565
|
bin: 'git',
|
|
563
566
|
args: ['status', '--porcelain', '--untracked-files=no'],
|
|
564
|
-
timeoutMs: 60_000
|
|
567
|
+
timeoutMs: 60_000,
|
|
568
|
+
...(signal === undefined ? {} : { signal })
|
|
565
569
|
});
|
|
566
570
|
return r.failedToStart || r.status !== 0 ? null : r.stdout;
|
|
567
571
|
};
|
|
568
|
-
const before = tracked();
|
|
569
|
-
const r = runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE, run);
|
|
572
|
+
const before = await tracked();
|
|
573
|
+
const r = await runVerifyCommandLine(cwd, command, DEBT_RERUN_TIMEOUT_MS, DEBT_INFRA_GAP_RE, run, signal);
|
|
570
574
|
if (r.outcome === 'fail')
|
|
571
575
|
return { outcome: 'fail', detail: `exit ${r.status} — ${r.tail}` };
|
|
572
576
|
if (r.outcome === 'gap')
|
|
573
577
|
return { outcome: 'gap', detail: r.detail };
|
|
574
|
-
const after = tracked();
|
|
578
|
+
const after = await tracked();
|
|
575
579
|
if (before === null || after === null) {
|
|
576
580
|
return { outcome: 'gap', detail: 'tracked-state guard could not read git status' };
|
|
577
581
|
}
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
* c.notFound()`): an existence guard is exactly how the bug presents (permanent
|
|
41
41
|
* 404), so guarded reads deliberately do NOT step aside.
|
|
42
42
|
*/
|
|
43
|
-
import { existsSync, readdirSync, readFileSync
|
|
43
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
44
44
|
import * as path from 'node:path';
|
|
45
|
+
import { shippedSources, stripCommentLines, SOURCE_HTML_RE, SOURCE_JS_RE } from './shipped-source.js';
|
|
45
46
|
export function emptyProducers() {
|
|
46
47
|
return {
|
|
47
48
|
files: new Set(),
|
|
@@ -83,14 +84,6 @@ const hasExt = (p) => /\.[A-Za-z0-9]{1,8}$/.test(path.posix.basename(p));
|
|
|
83
84
|
/** Source-only extensions nothing ships un-built: a missing one of these is hard
|
|
84
85
|
* evidence on its own (a script entrypoint like `bun src/server/index.ts`). */
|
|
85
86
|
const SOURCE_ONLY_EXT_RE = /\.(?:ts|tsx|mts|cts|jsx)$/i;
|
|
86
|
-
/** Strip comment-only lines (`// …`, `* …`) — a ref quoted in a comment is not a
|
|
87
|
-
* runtime read. Inline comments are left alone (strings may contain `//`). */
|
|
88
|
-
function stripCommentLines(src) {
|
|
89
|
-
return src
|
|
90
|
-
.split('\n')
|
|
91
|
-
.filter(l => !/^\s*(?:\/\/|\*|\/\*)/.test(l))
|
|
92
|
-
.join('\n');
|
|
93
|
-
}
|
|
94
87
|
// Literal-first-argument read-side constructs. `[^'"\`\n]` keeps the argument on
|
|
95
88
|
// one line and free of a closing quote; normalizeRefPath rejects `${…}` holes.
|
|
96
89
|
const JS_READ_PATTERNS = [
|
|
@@ -229,7 +222,7 @@ export function collectEmittedHtml(source) {
|
|
|
229
222
|
const writeRe = /\b(?:Bun\.write|writeFileSync|writeFile|fs\.promises\.writeFile)\(\s*(['"`])([^'"`\n]+)\1\s*,\s*/g;
|
|
230
223
|
for (let m = writeRe.exec(src); m !== null; m = writeRe.exec(src)) {
|
|
231
224
|
const docPath = normalizeRefPath(m[2]);
|
|
232
|
-
if (docPath === null || !
|
|
225
|
+
if (docPath === null || !SOURCE_HTML_RE.test(docPath))
|
|
233
226
|
continue;
|
|
234
227
|
const at = m.index + m[0].length;
|
|
235
228
|
let html;
|
|
@@ -777,58 +770,20 @@ export function resolveDanglingRefs(refs, prod, exists) {
|
|
|
777
770
|
out.push({ ...ref, reason });
|
|
778
771
|
}
|
|
779
772
|
}
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const MAX_SCAN_FILES = 3000;
|
|
789
|
-
const MAX_FILE_BYTES = 400_000;
|
|
790
|
-
/** Walk the tree for scannable sources (bounded, deterministic order). */
|
|
773
|
+
// The tree walk, the caps and the skip sets live in task/shipped-source.ts —
|
|
774
|
+
// this was serve-entry's walker written a second time, and the two skip sets had
|
|
775
|
+
// drifted: `bench`/`benchmarks` and `*.bench.*` were skipped there and scanned
|
|
776
|
+
// here, so a dangling reference in a benchmark was a run-level finding while the
|
|
777
|
+
// same file was invisible to the sibling scan.
|
|
778
|
+
//
|
|
779
|
+
// `producedDirs` stays this scan's own: bundled output re-referencing its own
|
|
780
|
+
// chunks is noise, and which dirs are produced is discovered per run.
|
|
791
781
|
function scanCandidates(cwd, prod) {
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
let entries;
|
|
798
|
-
try {
|
|
799
|
-
entries = readdirSync(path.join(cwd, rel)).sort();
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
for (const name of entries) {
|
|
805
|
-
if (out.length >= MAX_SCAN_FILES)
|
|
806
|
-
return;
|
|
807
|
-
const relPath = rel === '' ? name : `${rel}/${name}`;
|
|
808
|
-
let st;
|
|
809
|
-
try {
|
|
810
|
-
st = statSync(path.join(cwd, relPath));
|
|
811
|
-
}
|
|
812
|
-
catch {
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
if (st.isDirectory()) {
|
|
816
|
-
if (name.startsWith('.') || SKIP_DIR_RE.test(name))
|
|
817
|
-
continue;
|
|
818
|
-
if (rel === '' && producedDirs.has(name))
|
|
819
|
-
continue;
|
|
820
|
-
walk(relPath);
|
|
821
|
-
}
|
|
822
|
-
else if (st.isFile() && st.size <= MAX_FILE_BYTES) {
|
|
823
|
-
if (SKIP_FILE_RE.test(name))
|
|
824
|
-
continue;
|
|
825
|
-
if (SCAN_JS_RE.test(name) || SCAN_HTML_RE.test(name))
|
|
826
|
-
out.push(relPath);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
walk('');
|
|
831
|
-
return out;
|
|
782
|
+
const producedRoots = new Set([...prod.dirs, ...prod.opaque, ...prod.enumerable.keys()].map(d => d.split('/')[0]));
|
|
783
|
+
return shippedSources(cwd, {
|
|
784
|
+
ext: new RegExp(`${SOURCE_JS_RE.source}|${SOURCE_HTML_RE.source}`, 'i'),
|
|
785
|
+
excludeRoots: producedRoots
|
|
786
|
+
});
|
|
832
787
|
}
|
|
833
788
|
/**
|
|
834
789
|
* FINAL-GATE seam: scan the shipped tree for dangling runtime references.
|
|
@@ -855,7 +810,7 @@ export function findDanglingArtifacts(cwd) {
|
|
|
855
810
|
catch {
|
|
856
811
|
continue;
|
|
857
812
|
}
|
|
858
|
-
if (
|
|
813
|
+
if (SOURCE_HTML_RE.test(rel)) {
|
|
859
814
|
refs.push(...extractHtmlRefs(src, rel));
|
|
860
815
|
}
|
|
861
816
|
else {
|
|
@@ -901,7 +856,7 @@ export function collectGeneratedHtmlRefs(cwd) {
|
|
|
901
856
|
const production = discoverProducers(cwd, { excludeDevScripts: true });
|
|
902
857
|
const sources = [];
|
|
903
858
|
for (const rel of scanCandidates(cwd, full)) {
|
|
904
|
-
if (
|
|
859
|
+
if (SOURCE_HTML_RE.test(rel))
|
|
905
860
|
continue;
|
|
906
861
|
let src;
|
|
907
862
|
try {
|