@mjasnikovs/pi-task 0.40.50 → 0.42.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 +7 -4
- package/dist/config/config.d.ts +61 -18
- package/dist/config/config.js +50 -6
- package/dist/config/register.js +18 -5
- package/dist/shared/content-tokens.d.ts +16 -0
- package/dist/shared/content-tokens.js +137 -0
- package/dist/task/accept-debt.d.ts +31 -1
- package/dist/task/accept-debt.js +49 -3
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +66 -7
- package/dist/task/auto-io.js +152 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +184 -112
- package/dist/task/child-runner.d.ts +18 -0
- package/dist/task/child-runner.js +7 -8
- package/dist/task/command-run.d.ts +4 -0
- package/dist/task/command-run.js +4 -2
- package/dist/task/constraint-policy.d.ts +52 -0
- package/dist/task/constraint-policy.js +66 -0
- package/dist/task/context-silence.d.ts +5 -4
- package/dist/task/context-silence.js +13 -5
- package/dist/task/coverage-loop.d.ts +7 -4
- package/dist/task/coverage-loop.js +16 -138
- package/dist/task/debug-log.d.ts +25 -2
- package/dist/task/debug-log.js +64 -10
- package/dist/task/decompose-fidelity.d.ts +17 -4
- package/dist/task/decompose-fidelity.js +95 -55
- package/dist/task/decompose-granularity.d.ts +7 -7
- package/dist/task/decompose-granularity.js +8 -8
- package/dist/task/env-notes.d.ts +65 -19
- package/dist/task/env-notes.js +152 -50
- package/dist/task/external-context.js +25 -5
- package/dist/task/fix-context.d.ts +39 -0
- package/dist/task/fix-context.js +58 -0
- package/dist/task/gate-deps.d.ts +49 -0
- package/dist/task/gate-deps.js +201 -39
- package/dist/task/gate-evidence.d.ts +51 -0
- package/dist/task/gate-evidence.js +90 -0
- package/dist/task/gate-resolution.d.ts +119 -0
- package/dist/task/gate-resolution.js +120 -0
- package/dist/task/git-state-guard.js +3 -26
- package/dist/task/handoff.d.ts +35 -0
- package/dist/task/handoff.js +100 -0
- package/dist/task/health-baseline.d.ts +87 -0
- package/dist/task/health-baseline.js +158 -0
- package/dist/task/health-repair.d.ts +68 -0
- package/dist/task/health-repair.js +124 -0
- package/dist/task/ledger.d.ts +10 -0
- package/dist/task/ledger.js +22 -11
- package/dist/task/lint-fix.d.ts +11 -0
- package/dist/task/lint-fix.js +14 -8
- package/dist/task/loop-detector.d.ts +52 -3
- package/dist/task/loop-detector.js +81 -5
- package/dist/task/mentions.d.ts +17 -0
- package/dist/task/mentions.js +86 -0
- package/dist/task/orchestrator.d.ts +53 -9
- package/dist/task/orchestrator.js +104 -28
- package/dist/task/orientation.d.ts +135 -29
- package/dist/task/orientation.js +259 -67
- package/dist/task/owned-freeze-reassign.d.ts +7 -3
- package/dist/task/owned-freeze-reassign.js +13 -5
- package/dist/task/parsers.d.ts +6 -1
- package/dist/task/parsers.js +21 -4
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +125 -153
- package/dist/task/plan-orchestrator.js +5 -6
- package/dist/task/plan-rounds.d.ts +1 -0
- package/dist/task/plan-rounds.js +8 -1
- package/dist/task/prohibition-probe.d.ts +6 -1
- package/dist/task/prohibition-probe.js +26 -6
- package/dist/task/prompts.js +13 -2
- package/dist/task/qa-transcript.d.ts +17 -0
- package/dist/task/qa-transcript.js +28 -0
- package/dist/task/question-source.d.ts +16 -2
- package/dist/task/question-source.js +21 -8
- package/dist/task/repo-health-check.d.ts +27 -0
- package/dist/task/repo-health-check.js +39 -5
- package/dist/task/requirements.d.ts +67 -13
- package/dist/task/requirements.js +129 -67
- package/dist/task/research-worker.d.ts +43 -2
- package/dist/task/research-worker.js +103 -8
- package/dist/task/run-bracket.d.ts +9 -4
- package/dist/task/run-bracket.js +9 -0
- package/dist/task/run-context.d.ts +152 -0
- package/dist/task/run-context.js +277 -0
- package/dist/task/spec-doc.d.ts +78 -0
- package/dist/task/spec-doc.js +205 -0
- package/dist/task/spec-model.d.ts +70 -0
- package/dist/task/spec-model.js +139 -0
- package/dist/task/stall-detector.d.ts +1 -1
- package/dist/task/stall-detector.js +9 -5
- package/dist/task/state-dir.d.ts +29 -0
- package/dist/task/state-dir.js +80 -0
- package/dist/task/suppression-probe.d.ts +67 -0
- package/dist/task/suppression-probe.js +78 -0
- package/dist/task/task-gates.d.ts +52 -58
- package/dist/task/task-gates.js +161 -199
- package/dist/task/task-io.d.ts +10 -0
- package/dist/task/task-io.js +69 -18
- package/dist/task/task-parsers.js +6 -6
- package/dist/task/task-types.d.ts +7 -0
- package/dist/task/timings.d.ts +11 -0
- package/dist/task/timings.js +21 -0
- package/dist/task/tree-hash.d.ts +8 -0
- package/dist/task/tree-hash.js +55 -0
- package/dist/task/verify-reconcile.d.ts +4 -3
- package/dist/task/verify-reconcile.js +7 -8
- package/dist/task/verify-resolution.d.ts +32 -6
- package/dist/task/verify-resolution.js +78 -15
- package/dist/task/verify-work.d.ts +105 -35
- package/dist/task/verify-work.js +232 -60
- package/dist/task/yolo.d.ts +0 -13
- package/dist/task/yolo.js +9 -21
- package/dist/workers/pi-worker-core.d.ts +20 -0
- package/dist/workers/pi-worker-core.js +16 -11
- package/dist/workers/pi-worker-docs.d.ts +6 -5
- package/dist/workers/pi-worker-docs.js +8 -7
- package/dist/workers/pi-worker-fetch.d.ts +3 -2
- package/dist/workers/pi-worker-fetch.js +5 -4
- package/dist/workers/pi-worker-search.js +5 -4
- package/dist/workers/research-cache.d.ts +23 -1
- package/dist/workers/research-cache.js +63 -3
- package/dist/workers/shared.d.ts +18 -0
- package/dist/workers/shared.js +38 -22
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { SpawnFn } from '../shared/child-process.js';
|
|
2
|
+
import { type EcosystemId } from '../workers/docs-ecosystems.js';
|
|
3
|
+
import type { GateEvidence } from './gate-evidence.js';
|
|
4
|
+
import { type OrientationResult } from './orientation.js';
|
|
5
|
+
/**
|
|
6
|
+
* What a verified command is FOR, and the only column that decides whether the
|
|
7
|
+
* gate-evidence runner may execute it: `check` and `build` terminate on their own,
|
|
8
|
+
* `serve` does not and is never launched.
|
|
9
|
+
*/
|
|
10
|
+
export type ToolingClass = 'check' | 'build' | 'serve';
|
|
11
|
+
/**
|
|
12
|
+
* The exit code of a command nobody has run. verify-tooling confirms a command
|
|
13
|
+
* from STATIC evidence (a script in the manifest, a binary in node_modules/.bin)
|
|
14
|
+
* and is forbidden to execute it, so its entries carry this until the evidence
|
|
15
|
+
* runner replaces them with a real result.
|
|
16
|
+
*/
|
|
17
|
+
export declare const NOT_RUN = -1;
|
|
18
|
+
/** One command this run has verified, with the manifest state that vouched for it. */
|
|
19
|
+
export interface VerifiedCommand {
|
|
20
|
+
cmd: string;
|
|
21
|
+
/** Where it runs — the repo root today; a monorepo package tomorrow. */
|
|
22
|
+
cwd: string;
|
|
23
|
+
class: ToolingClass;
|
|
24
|
+
/** {@link NOT_RUN} until something actually runs it. */
|
|
25
|
+
exitCode: number;
|
|
26
|
+
verifiedAt: number;
|
|
27
|
+
manifestHash: string;
|
|
28
|
+
}
|
|
29
|
+
/** The worktree content as a git tree object (see tree-hash.ts), or null when
|
|
30
|
+
* git cannot say. */
|
|
31
|
+
export declare function treeHash(cwd: string, opts?: {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
spawnFn?: SpawnFn;
|
|
34
|
+
}): Promise<string | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Content hash of the files that decide which commands this project HAS — the
|
|
37
|
+
* manifests `discoverHealthCommands` reads, plus the Makefile. A missing file
|
|
38
|
+
* contributes nothing, and since each contributing file is hashed under its own
|
|
39
|
+
* name, adding or deleting one moves the hash.
|
|
40
|
+
*/
|
|
41
|
+
export declare function manifestHash(cwd: string): Promise<string>;
|
|
42
|
+
/** Produce one tree's gate evidence, given the commands that may be run against it
|
|
43
|
+
* and the hash of the tree they are being run against (see gate-evidence.ts). */
|
|
44
|
+
export type EvidenceRunner = (commands: readonly VerifiedCommand[], treeHash: string | null) => Promise<GateEvidence>;
|
|
45
|
+
/** A verify-tooling verdict as the child reported it, before it is dated and stored. */
|
|
46
|
+
export interface ToolingVerdict {
|
|
47
|
+
cmd: string;
|
|
48
|
+
class: ToolingClass;
|
|
49
|
+
}
|
|
50
|
+
/** Everything the run-level tooling cache asks of its verifier. */
|
|
51
|
+
export type VerifyToolingFn = (commands: string[]) => Promise<{
|
|
52
|
+
verified: ToolingVerdict[];
|
|
53
|
+
rejected: string[];
|
|
54
|
+
}>;
|
|
55
|
+
export interface RunContextOptions {
|
|
56
|
+
cwd: string;
|
|
57
|
+
/** Reuse an existing id (a resumed /task-auto run); minted when absent. */
|
|
58
|
+
runId?: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The per-run facts, computed on first ask and then held.
|
|
63
|
+
*
|
|
64
|
+
* Every getter is memoised on the PROMISE, not on the value, so two phases asking
|
|
65
|
+
* at once still produce one computation.
|
|
66
|
+
*/
|
|
67
|
+
export declare class RunContext {
|
|
68
|
+
readonly cwd: string;
|
|
69
|
+
readonly runId: string;
|
|
70
|
+
private readonly _signal;
|
|
71
|
+
private _inventory;
|
|
72
|
+
private readonly _orientation;
|
|
73
|
+
private _orientationExclude;
|
|
74
|
+
private _manifestDeps;
|
|
75
|
+
private _ecosystems;
|
|
76
|
+
private _verifiedTooling;
|
|
77
|
+
/** Commands verify-tooling has already refused under `_toolingHash`. */
|
|
78
|
+
private _rejectedTooling;
|
|
79
|
+
private _toolingHash;
|
|
80
|
+
private _evidence;
|
|
81
|
+
private _evidenceQueue;
|
|
82
|
+
constructor(opts: RunContextOptions);
|
|
83
|
+
/** `git ls-files` for this run; '' outside a git tree (see file-inventory.ts). */
|
|
84
|
+
inventory(): Promise<string>;
|
|
85
|
+
/** This run's inventory as paths, blank lines dropped. */
|
|
86
|
+
inventoryPaths(): Promise<string[]>;
|
|
87
|
+
/**
|
|
88
|
+
* The orientation core over this run's whole inventory — the block the
|
|
89
|
+
* exploring research workers get, built once however many tasks ask.
|
|
90
|
+
*
|
|
91
|
+
* Memoised per CITED SET, not once: the files a task points at are part of the
|
|
92
|
+
* question, and two tasks citing different specs are asking for different
|
|
93
|
+
* blocks. Every task in a /task-auto run carries the same threaded spec ref, so
|
|
94
|
+
* in practice this is still one build.
|
|
95
|
+
*/
|
|
96
|
+
orientation(cited?: readonly string[]): Promise<OrientationResult>;
|
|
97
|
+
/** The same block over a NARROWED candidate list (refine takes the manifest and
|
|
98
|
+
* config tiers only), which is per-caller and so not memoised. */
|
|
99
|
+
orientationOf(paths: string[] | undefined, cited?: readonly string[]): Promise<OrientationResult>;
|
|
100
|
+
/**
|
|
101
|
+
* What this project says is not its own source, on top of `VENDORED_DIRS`:
|
|
102
|
+
* its `.gitignore`, plus the user's `orientationExclude`. Read once — a run
|
|
103
|
+
* that re-read it mid-way would orient two tasks differently.
|
|
104
|
+
*/
|
|
105
|
+
private orientationExclusions;
|
|
106
|
+
/**
|
|
107
|
+
* Dependency names this project's manifests declare, across every ecosystem it
|
|
108
|
+
* is a project of. `[]` when no manifest could be read — callers use it to tell
|
|
109
|
+
* "this bullet is about a library" from "this bullet is about our source", and
|
|
110
|
+
* that degrades to a no-op rather than an error.
|
|
111
|
+
*/
|
|
112
|
+
manifestDeps(): string[];
|
|
113
|
+
ecosystems(): EcosystemId[];
|
|
114
|
+
/** What this run has verified so far, newest verdict per command. */
|
|
115
|
+
get verifiedTooling(): readonly VerifiedCommand[];
|
|
116
|
+
/**
|
|
117
|
+
* The verified subset of `commands`, running `verify` only for the ones this
|
|
118
|
+
* run has no verdict for.
|
|
119
|
+
*
|
|
120
|
+
* A manifest edit — a new script, a new Makefile target — is the only thing that
|
|
121
|
+
* can change which commands the project has, so it is the only thing that drops
|
|
122
|
+
* the verdicts. Short of that, the second task's identical TOOLING list is
|
|
123
|
+
* answered from the first task's child.
|
|
124
|
+
*/
|
|
125
|
+
verifiedToolingFor(commands: string[], verify: VerifyToolingFn): Promise<VerifiedCommand[]>;
|
|
126
|
+
/**
|
|
127
|
+
* This gate session's evidence: the run's verified check and build commands,
|
|
128
|
+
* run against the CURRENT tree, at most once per tree (see gate-evidence.ts).
|
|
129
|
+
*
|
|
130
|
+
* The TREE HASH is the key, so a lint-fix or an autofix that changes the tree
|
|
131
|
+
* costs exactly one re-run and a second gate child on the same tree costs none.
|
|
132
|
+
* A tree git cannot hash is never cached: "unchanged" is not something we could
|
|
133
|
+
* claim about it.
|
|
134
|
+
*/
|
|
135
|
+
gateEvidenceFor(produce: EvidenceRunner): Promise<GateEvidence>;
|
|
136
|
+
private freshEvidence;
|
|
137
|
+
}
|
|
138
|
+
/** Open a run: every task inside it now shares one context. Nests — an inner
|
|
139
|
+
* bracket returns the outer run's context untouched. */
|
|
140
|
+
export declare function openRunContext(cwd: string, runId?: string): RunContext;
|
|
141
|
+
/** Close the run `rc` opened. A stale close (an inner bracket ending after the
|
|
142
|
+
* outer one) is a no-op. */
|
|
143
|
+
export declare function closeRunContext(rc: RunContext): void;
|
|
144
|
+
/**
|
|
145
|
+
* The open run's context, or a fresh one for a caller outside any bracket (a
|
|
146
|
+
* direct `runSingleTask`, a test). The fallback is deliberately NOT registered:
|
|
147
|
+
* an unbracketed caller has no run to end, and a context that outlived its caller
|
|
148
|
+
* would serve a later run a stale inventory.
|
|
149
|
+
*/
|
|
150
|
+
export declare function currentRunContext(cwd: string): RunContext;
|
|
151
|
+
/** The open run's id for `cwd`, or null outside any bracket. */
|
|
152
|
+
export declare function openRunId(cwd: string): string | null;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-context — what is true of the RUN rather than of the task inside it.
|
|
3
|
+
*
|
|
4
|
+
* A /task-auto run is many task pipelines over one unchanging project. The file
|
|
5
|
+
* inventory, the orientation core, the manifest's dependency names, the detected
|
|
6
|
+
* ecosystems and the verified tooling commands are all facts about the PROJECT,
|
|
7
|
+
* and every one of them was being re-derived per task: twenty-one verify-tooling
|
|
8
|
+
* children asking the same question of the same package.json, each free to answer
|
|
9
|
+
* differently.
|
|
10
|
+
*
|
|
11
|
+
* So they are held here, once per run, and handed to the phases through
|
|
12
|
+
* `PhaseDeps.runContext`. A bare `/task` is a run of one task and gets its own
|
|
13
|
+
* context with a minted id, so nothing has two code paths.
|
|
14
|
+
*
|
|
15
|
+
* INVALIDATION. Verified tooling is invalidated by the MANIFEST HASH alone — the
|
|
16
|
+
* content of the files `discoverHealthCommands` reads. A task that edits source
|
|
17
|
+
* cannot change which commands exist; a task that adds a script can, and that is
|
|
18
|
+
* exactly what the hash moves on. Everything else here describes the tree as the
|
|
19
|
+
* run found it and is not re-read: a mid-run inventory refresh would hand two
|
|
20
|
+
* tasks different orientation cores for the same question.
|
|
21
|
+
*
|
|
22
|
+
* TREE HASH is the other half, used by the gate-evidence cache; its one
|
|
23
|
+
* implementation lives in tree-hash.ts.
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import * as fsp from 'node:fs/promises';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { makeGit } from '../shared/git-runner.js';
|
|
29
|
+
import { declaredDepNames, detectEcosystems } from '../workers/docs-ecosystems.js';
|
|
30
|
+
import { newRunToken } from '../workers/research-cache.js';
|
|
31
|
+
import { getFileInventory } from './file-inventory.js';
|
|
32
|
+
import { getConfig } from '../config/config.js';
|
|
33
|
+
import { buildOrientation, parseIgnorePatterns } from './orientation.js';
|
|
34
|
+
import { HEALTH_MANIFEST_FILES } from './repo-health-check.js';
|
|
35
|
+
import { worktreeTreeHash } from './tree-hash.js';
|
|
36
|
+
/**
|
|
37
|
+
* The exit code of a command nobody has run. verify-tooling confirms a command
|
|
38
|
+
* from STATIC evidence (a script in the manifest, a binary in node_modules/.bin)
|
|
39
|
+
* and is forbidden to execute it, so its entries carry this until the evidence
|
|
40
|
+
* runner replaces them with a real result.
|
|
41
|
+
*/
|
|
42
|
+
export const NOT_RUN = -1;
|
|
43
|
+
/** The worktree content as a git tree object (see tree-hash.ts), or null when
|
|
44
|
+
* git cannot say. */
|
|
45
|
+
export async function treeHash(cwd, opts = {}) {
|
|
46
|
+
return worktreeTreeHash(makeGit(cwd, opts.signal, opts.spawnFn));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Content hash of the files that decide which commands this project HAS — the
|
|
50
|
+
* manifests `discoverHealthCommands` reads, plus the Makefile. A missing file
|
|
51
|
+
* contributes nothing, and since each contributing file is hashed under its own
|
|
52
|
+
* name, adding or deleting one moves the hash.
|
|
53
|
+
*/
|
|
54
|
+
export async function manifestHash(cwd) {
|
|
55
|
+
const h = createHash('sha256');
|
|
56
|
+
for (const file of HEALTH_MANIFEST_FILES) {
|
|
57
|
+
try {
|
|
58
|
+
h.update(file);
|
|
59
|
+
h.update('\u0000');
|
|
60
|
+
h.update(await fsp.readFile(path.join(cwd, file)));
|
|
61
|
+
h.update('\u0000');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// absent or unreadable ⇒ contributes nothing
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return h.digest('hex');
|
|
68
|
+
}
|
|
69
|
+
/** The classes the gate-evidence runner may execute. `serve` is absent by
|
|
70
|
+
* definition: it never returns, and the parent has nothing to kill it with. */
|
|
71
|
+
const EVIDENCE_CLASSES = new Set(['check', 'build']);
|
|
72
|
+
/**
|
|
73
|
+
* The per-run facts, computed on first ask and then held.
|
|
74
|
+
*
|
|
75
|
+
* Every getter is memoised on the PROMISE, not on the value, so two phases asking
|
|
76
|
+
* at once still produce one computation.
|
|
77
|
+
*/
|
|
78
|
+
export class RunContext {
|
|
79
|
+
cwd;
|
|
80
|
+
runId;
|
|
81
|
+
_signal;
|
|
82
|
+
_inventory;
|
|
83
|
+
_orientation = new Map();
|
|
84
|
+
_orientationExclude;
|
|
85
|
+
_manifestDeps;
|
|
86
|
+
_ecosystems;
|
|
87
|
+
_verifiedTooling = [];
|
|
88
|
+
/** Commands verify-tooling has already refused under `_toolingHash`. */
|
|
89
|
+
_rejectedTooling = new Set();
|
|
90
|
+
_toolingHash;
|
|
91
|
+
_evidence;
|
|
92
|
+
_evidenceQueue = Promise.resolve();
|
|
93
|
+
constructor(opts) {
|
|
94
|
+
this.cwd = opts.cwd;
|
|
95
|
+
this.runId = opts.runId ?? newRunToken();
|
|
96
|
+
this._signal = opts.signal;
|
|
97
|
+
}
|
|
98
|
+
/** `git ls-files` for this run; '' outside a git tree (see file-inventory.ts). */
|
|
99
|
+
inventory() {
|
|
100
|
+
this._inventory ??= getFileInventory(this.cwd, this._signal).catch(() => '');
|
|
101
|
+
return this._inventory;
|
|
102
|
+
}
|
|
103
|
+
/** This run's inventory as paths, blank lines dropped. */
|
|
104
|
+
async inventoryPaths() {
|
|
105
|
+
return (await this.inventory()).split('\n').filter(l => l.trim().length > 0);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The orientation core over this run's whole inventory — the block the
|
|
109
|
+
* exploring research workers get, built once however many tasks ask.
|
|
110
|
+
*
|
|
111
|
+
* Memoised per CITED SET, not once: the files a task points at are part of the
|
|
112
|
+
* question, and two tasks citing different specs are asking for different
|
|
113
|
+
* blocks. Every task in a /task-auto run carries the same threaded spec ref, so
|
|
114
|
+
* in practice this is still one build.
|
|
115
|
+
*/
|
|
116
|
+
orientation(cited = []) {
|
|
117
|
+
const key = JSON.stringify([...cited].sort());
|
|
118
|
+
let built = this._orientation.get(key);
|
|
119
|
+
if (!built) {
|
|
120
|
+
built = this.orientationOf(undefined, cited);
|
|
121
|
+
this._orientation.set(key, built);
|
|
122
|
+
}
|
|
123
|
+
return built;
|
|
124
|
+
}
|
|
125
|
+
/** The same block over a NARROWED candidate list (refine takes the manifest and
|
|
126
|
+
* config tiers only), which is per-caller and so not memoised. */
|
|
127
|
+
async orientationOf(paths, cited = []) {
|
|
128
|
+
const candidates = paths ?? (await this.inventoryPaths());
|
|
129
|
+
return buildOrientation(candidates, async (p) => {
|
|
130
|
+
try {
|
|
131
|
+
return await fsp.readFile(path.resolve(this.cwd, p), 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}, { cited, excludePatterns: await this.orientationExclusions() }).catch(() => ({ block: '', supplied: new Set() }));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What this project says is not its own source, on top of `VENDORED_DIRS`:
|
|
140
|
+
* its `.gitignore`, plus the user's `orientationExclude`. Read once — a run
|
|
141
|
+
* that re-read it mid-way would orient two tasks differently.
|
|
142
|
+
*/
|
|
143
|
+
orientationExclusions() {
|
|
144
|
+
this._orientationExclude ??= fsp
|
|
145
|
+
.readFile(path.join(this.cwd, '.gitignore'), 'utf8')
|
|
146
|
+
.then(parseIgnorePatterns)
|
|
147
|
+
.catch(() => [])
|
|
148
|
+
.then(fromGit => [...fromGit, ...getConfig().orientationExclude]);
|
|
149
|
+
return this._orientationExclude;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Dependency names this project's manifests declare, across every ecosystem it
|
|
153
|
+
* is a project of. `[]` when no manifest could be read — callers use it to tell
|
|
154
|
+
* "this bullet is about a library" from "this bullet is about our source", and
|
|
155
|
+
* that degrades to a no-op rather than an error.
|
|
156
|
+
*/
|
|
157
|
+
manifestDeps() {
|
|
158
|
+
this._manifestDeps ??= [...(declaredDepNames(this.cwd) ?? [])];
|
|
159
|
+
return this._manifestDeps;
|
|
160
|
+
}
|
|
161
|
+
ecosystems() {
|
|
162
|
+
this._ecosystems ??= detectEcosystems(this.cwd);
|
|
163
|
+
return this._ecosystems;
|
|
164
|
+
}
|
|
165
|
+
/** What this run has verified so far, newest verdict per command. */
|
|
166
|
+
get verifiedTooling() {
|
|
167
|
+
return this._verifiedTooling;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The verified subset of `commands`, running `verify` only for the ones this
|
|
171
|
+
* run has no verdict for.
|
|
172
|
+
*
|
|
173
|
+
* A manifest edit — a new script, a new Makefile target — is the only thing that
|
|
174
|
+
* can change which commands the project has, so it is the only thing that drops
|
|
175
|
+
* the verdicts. Short of that, the second task's identical TOOLING list is
|
|
176
|
+
* answered from the first task's child.
|
|
177
|
+
*/
|
|
178
|
+
async verifiedToolingFor(commands, verify) {
|
|
179
|
+
const hash = await manifestHash(this.cwd);
|
|
180
|
+
if (hash !== this._toolingHash) {
|
|
181
|
+
this._verifiedTooling = [];
|
|
182
|
+
this._rejectedTooling = new Set();
|
|
183
|
+
this._toolingHash = hash;
|
|
184
|
+
}
|
|
185
|
+
const decided = new Set([
|
|
186
|
+
...this._verifiedTooling.map(v => v.cmd),
|
|
187
|
+
...this._rejectedTooling
|
|
188
|
+
]);
|
|
189
|
+
const unknown = commands.filter(c => !decided.has(c));
|
|
190
|
+
if (unknown.length > 0) {
|
|
191
|
+
const r = await verify(unknown);
|
|
192
|
+
const at = Date.now();
|
|
193
|
+
for (const v of r.verified) {
|
|
194
|
+
this._verifiedTooling.push({
|
|
195
|
+
cmd: v.cmd,
|
|
196
|
+
cwd: this.cwd,
|
|
197
|
+
class: v.class,
|
|
198
|
+
exitCode: NOT_RUN,
|
|
199
|
+
verifiedAt: at,
|
|
200
|
+
manifestHash: hash
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
for (const c of r.rejected)
|
|
204
|
+
this._rejectedTooling.add(c);
|
|
205
|
+
}
|
|
206
|
+
const wanted = new Set(commands);
|
|
207
|
+
return this._verifiedTooling.filter(v => wanted.has(v.cmd));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* This gate session's evidence: the run's verified check and build commands,
|
|
211
|
+
* run against the CURRENT tree, at most once per tree (see gate-evidence.ts).
|
|
212
|
+
*
|
|
213
|
+
* The TREE HASH is the key, so a lint-fix or an autofix that changes the tree
|
|
214
|
+
* costs exactly one re-run and a second gate child on the same tree costs none.
|
|
215
|
+
* A tree git cannot hash is never cached: "unchanged" is not something we could
|
|
216
|
+
* claim about it.
|
|
217
|
+
*/
|
|
218
|
+
gateEvidenceFor(produce) {
|
|
219
|
+
// Serialised rather than merely memoised: two gate children asking at once
|
|
220
|
+
// is exactly the duplicate suite run this cache exists to kill.
|
|
221
|
+
const next = this._evidenceQueue.then(() => this.freshEvidence(produce));
|
|
222
|
+
this._evidenceQueue = next.catch(() => { });
|
|
223
|
+
return next;
|
|
224
|
+
}
|
|
225
|
+
async freshEvidence(produce) {
|
|
226
|
+
const hash = await treeHash(this.cwd, this._signal ? { signal: this._signal } : {});
|
|
227
|
+
if (hash !== null && this._evidence?.hash === hash)
|
|
228
|
+
return this._evidence.value;
|
|
229
|
+
const value = await produce(this._verifiedTooling.filter(v => EVIDENCE_CLASSES.has(v.class)), hash);
|
|
230
|
+
// {@link NOT_RUN} stands until something runs the command. This is that
|
|
231
|
+
// something, so the verdicts stop claiming nobody has.
|
|
232
|
+
for (const c of value.commands) {
|
|
233
|
+
const verified = this._verifiedTooling.find(v => v.cmd === c.cmd);
|
|
234
|
+
if (verified)
|
|
235
|
+
verified.exitCode = c.exitCode;
|
|
236
|
+
}
|
|
237
|
+
if (hash !== null)
|
|
238
|
+
this._evidence = { hash, value };
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The context of the run that owns the session right now, set by the run bracket.
|
|
244
|
+
*
|
|
245
|
+
* A module-level holder rather than a parameter because the path from a command
|
|
246
|
+
* handler to a phase runs through `runTask`, a seam whose signature is shared with
|
|
247
|
+
* the gates; threading the context through it would put a field on two dep bags
|
|
248
|
+
* that nothing but this would ever set.
|
|
249
|
+
*/
|
|
250
|
+
let currentRun = null;
|
|
251
|
+
/** Open a run: every task inside it now shares one context. Nests — an inner
|
|
252
|
+
* bracket returns the outer run's context untouched. */
|
|
253
|
+
export function openRunContext(cwd, runId) {
|
|
254
|
+
if (currentRun && currentRun.cwd === cwd)
|
|
255
|
+
return currentRun;
|
|
256
|
+
currentRun = new RunContext({ cwd, ...(runId === undefined ? {} : { runId }) });
|
|
257
|
+
return currentRun;
|
|
258
|
+
}
|
|
259
|
+
/** Close the run `rc` opened. A stale close (an inner bracket ending after the
|
|
260
|
+
* outer one) is a no-op. */
|
|
261
|
+
export function closeRunContext(rc) {
|
|
262
|
+
if (currentRun === rc)
|
|
263
|
+
currentRun = null;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The open run's context, or a fresh one for a caller outside any bracket (a
|
|
267
|
+
* direct `runSingleTask`, a test). The fallback is deliberately NOT registered:
|
|
268
|
+
* an unbracketed caller has no run to end, and a context that outlived its caller
|
|
269
|
+
* would serve a later run a stale inventory.
|
|
270
|
+
*/
|
|
271
|
+
export function currentRunContext(cwd) {
|
|
272
|
+
return currentRun && currentRun.cwd === cwd ? currentRun : new RunContext({ cwd });
|
|
273
|
+
}
|
|
274
|
+
/** The open run's id for `cwd`, or null outside any bracket. */
|
|
275
|
+
export function openRunId(cwd) {
|
|
276
|
+
return currentRun && currentRun.cwd === cwd ? currentRun.runId : null;
|
|
277
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type BlockKind = 'para' | 'list-item' | 'table-row' | 'fence' | 'quote';
|
|
2
|
+
export interface Block {
|
|
3
|
+
kind: BlockKind;
|
|
4
|
+
/** The source lines, verbatim. */
|
|
5
|
+
text: string;
|
|
6
|
+
/** `text` with markdown MARKUP removed — see `demark`. */
|
|
7
|
+
plain: string;
|
|
8
|
+
/** 1-based line of the block's first line; the anchor a grounded quote records. */
|
|
9
|
+
line: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Section {
|
|
12
|
+
/** The heading text without its `#` run. */
|
|
13
|
+
heading: string;
|
|
14
|
+
depth: number;
|
|
15
|
+
blocks: Block[];
|
|
16
|
+
}
|
|
17
|
+
export interface SpecDoc {
|
|
18
|
+
/** Blocks above the first heading. Empty unless the doc has headings at all. */
|
|
19
|
+
preamble: Block[];
|
|
20
|
+
sections: Section[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Markdown MARKUP dropped before grounding — emphasis runs, list and heading
|
|
24
|
+
* markers, blockquote markers, table pipes and CODE BACKTICKS. Not content: no
|
|
25
|
+
* word, number or punctuation inside a sentence is touched, so this cannot make
|
|
26
|
+
* an invented quote match.
|
|
27
|
+
*
|
|
28
|
+
* WHY. A model copies a spec line as it READS, and what it reads is rendered:
|
|
29
|
+
* `2. **Auth** — sessions, login/logout/me, guards + tests.` comes back as
|
|
30
|
+
* `Auth — sessions, login/logout/me, guards + tests.` That is a verbatim copy of
|
|
31
|
+
* the line's TEXT, and the exact-substring test called it fabricated and threw
|
|
32
|
+
* it away — including, as here, the "+ tests" line that is the restoration
|
|
33
|
+
* module's own worked example.
|
|
34
|
+
*
|
|
35
|
+
* BACKTICKS ARE THE SAME CLASS and were the larger half. A code span renders as
|
|
36
|
+
* bare text, so `3. **Invites** — create/validate/redeem, \`/join/:token\` page.`
|
|
37
|
+
* comes back as `Invites — create/validate/redeem, /join/:token page.` Screening
|
|
38
|
+
* every spec line in its RENDERED form is what makes those quotes match at all.
|
|
39
|
+
*
|
|
40
|
+
* The two directions this has to hold in, both run:
|
|
41
|
+
* FLOOR a real spec line with ONE content word altered must NOT be grounded —
|
|
42
|
+
* changing `sessions` to `tokens`, or `redeem` to `revoke`, drops it.
|
|
43
|
+
* CEILING a real spec line quoted without its markup MUST be grounded — both the
|
|
44
|
+
* `2. **Auth** —` numbering-and-bold case and the backticked
|
|
45
|
+
* `` `/join/:token` `` case still match.
|
|
46
|
+
*/
|
|
47
|
+
export declare function demark(s: string): string;
|
|
48
|
+
export declare function parseSpecDoc(text: string): SpecDoc;
|
|
49
|
+
/** Every block, in document order. */
|
|
50
|
+
export declare function blocksOf(doc: SpecDoc): Block[];
|
|
51
|
+
/**
|
|
52
|
+
* The blocks above the first CONTENT section — where a spec says what the thing
|
|
53
|
+
* IS rather than what the work must do.
|
|
54
|
+
*
|
|
55
|
+
* Two shapes, both real. A doc can open with prose and then head its sections; or
|
|
56
|
+
* it can open with its TITLE as the only top-level heading and put the same prose
|
|
57
|
+
* under it, which is what `# MX-5 Private — Project Design` does. A lone depth-1
|
|
58
|
+
* heading above deeper ones is a title, not a section, so its body is preamble
|
|
59
|
+
* too.
|
|
60
|
+
*
|
|
61
|
+
* A document with NO sections at all has no preamble: "preamble" means "above the
|
|
62
|
+
* body", so without a body there is nothing to be above — otherwise a heading-less
|
|
63
|
+
* prose spec would have its whole content excluded by any preamble policy.
|
|
64
|
+
*/
|
|
65
|
+
export declare function preambleOf(doc: SpecDoc): Block[];
|
|
66
|
+
/** Heading-delimited section texts in `plain` form, pre-normalised for
|
|
67
|
+
* containment tests. The preamble is its own leading section. */
|
|
68
|
+
export declare function sectionPlains(doc: SpecDoc): string[];
|
|
69
|
+
/** The whole doc in `plain` form — the flat fallback for a quote that spans
|
|
70
|
+
* block boundaries (a model quoting two consecutive bullets as one line). */
|
|
71
|
+
export declare function specPlain(doc: SpecDoc): string;
|
|
72
|
+
/**
|
|
73
|
+
* The first block whose `plain` contains `quote`, both demarked and normalised —
|
|
74
|
+
* the anchor a grounded quote records. `accept` narrows candidacy: a quote whose
|
|
75
|
+
* only match lies outside it is NOT grounded, which is how a requirement policy
|
|
76
|
+
* rejects a DDL column or a table cell the model quoted as an obligation.
|
|
77
|
+
*/
|
|
78
|
+
export declare function groundIn(doc: SpecDoc, quote: string, accept?: (b: Block) => boolean): Block | null;
|