@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.
Files changed (125) hide show
  1. package/README.md +7 -4
  2. package/dist/config/config.d.ts +61 -18
  3. package/dist/config/config.js +50 -6
  4. package/dist/config/register.js +18 -5
  5. package/dist/shared/content-tokens.d.ts +16 -0
  6. package/dist/shared/content-tokens.js +137 -0
  7. package/dist/task/accept-debt.d.ts +31 -1
  8. package/dist/task/accept-debt.js +49 -3
  9. package/dist/task/artifact-closure.js +7 -1
  10. package/dist/task/auto-io.d.ts +66 -7
  11. package/dist/task/auto-io.js +152 -47
  12. package/dist/task/auto-orchestrator.d.ts +22 -19
  13. package/dist/task/auto-orchestrator.js +184 -112
  14. package/dist/task/child-runner.d.ts +18 -0
  15. package/dist/task/child-runner.js +7 -8
  16. package/dist/task/command-run.d.ts +4 -0
  17. package/dist/task/command-run.js +4 -2
  18. package/dist/task/constraint-policy.d.ts +52 -0
  19. package/dist/task/constraint-policy.js +66 -0
  20. package/dist/task/context-silence.d.ts +5 -4
  21. package/dist/task/context-silence.js +13 -5
  22. package/dist/task/coverage-loop.d.ts +7 -4
  23. package/dist/task/coverage-loop.js +16 -138
  24. package/dist/task/debug-log.d.ts +25 -2
  25. package/dist/task/debug-log.js +64 -10
  26. package/dist/task/decompose-fidelity.d.ts +17 -4
  27. package/dist/task/decompose-fidelity.js +95 -55
  28. package/dist/task/decompose-granularity.d.ts +7 -7
  29. package/dist/task/decompose-granularity.js +8 -8
  30. package/dist/task/env-notes.d.ts +65 -19
  31. package/dist/task/env-notes.js +152 -50
  32. package/dist/task/external-context.js +25 -5
  33. package/dist/task/fix-context.d.ts +39 -0
  34. package/dist/task/fix-context.js +58 -0
  35. package/dist/task/gate-deps.d.ts +49 -0
  36. package/dist/task/gate-deps.js +201 -39
  37. package/dist/task/gate-evidence.d.ts +51 -0
  38. package/dist/task/gate-evidence.js +90 -0
  39. package/dist/task/gate-resolution.d.ts +119 -0
  40. package/dist/task/gate-resolution.js +120 -0
  41. package/dist/task/git-state-guard.js +3 -26
  42. package/dist/task/handoff.d.ts +35 -0
  43. package/dist/task/handoff.js +100 -0
  44. package/dist/task/health-baseline.d.ts +87 -0
  45. package/dist/task/health-baseline.js +158 -0
  46. package/dist/task/health-repair.d.ts +68 -0
  47. package/dist/task/health-repair.js +124 -0
  48. package/dist/task/ledger.d.ts +10 -0
  49. package/dist/task/ledger.js +22 -11
  50. package/dist/task/lint-fix.d.ts +11 -0
  51. package/dist/task/lint-fix.js +14 -8
  52. package/dist/task/loop-detector.d.ts +52 -3
  53. package/dist/task/loop-detector.js +81 -5
  54. package/dist/task/mentions.d.ts +17 -0
  55. package/dist/task/mentions.js +86 -0
  56. package/dist/task/orchestrator.d.ts +53 -9
  57. package/dist/task/orchestrator.js +104 -28
  58. package/dist/task/orientation.d.ts +135 -29
  59. package/dist/task/orientation.js +259 -67
  60. package/dist/task/owned-freeze-reassign.d.ts +7 -3
  61. package/dist/task/owned-freeze-reassign.js +13 -5
  62. package/dist/task/parsers.d.ts +6 -1
  63. package/dist/task/parsers.js +21 -4
  64. package/dist/task/phases.d.ts +1 -1
  65. package/dist/task/phases.js +125 -153
  66. package/dist/task/plan-orchestrator.js +5 -6
  67. package/dist/task/plan-rounds.d.ts +1 -0
  68. package/dist/task/plan-rounds.js +8 -1
  69. package/dist/task/prohibition-probe.d.ts +6 -1
  70. package/dist/task/prohibition-probe.js +26 -6
  71. package/dist/task/prompts.js +13 -2
  72. package/dist/task/qa-transcript.d.ts +17 -0
  73. package/dist/task/qa-transcript.js +28 -0
  74. package/dist/task/question-source.d.ts +16 -2
  75. package/dist/task/question-source.js +21 -8
  76. package/dist/task/repo-health-check.d.ts +27 -0
  77. package/dist/task/repo-health-check.js +39 -5
  78. package/dist/task/requirements.d.ts +67 -13
  79. package/dist/task/requirements.js +129 -67
  80. package/dist/task/research-worker.d.ts +43 -2
  81. package/dist/task/research-worker.js +103 -8
  82. package/dist/task/run-bracket.d.ts +9 -4
  83. package/dist/task/run-bracket.js +9 -0
  84. package/dist/task/run-context.d.ts +152 -0
  85. package/dist/task/run-context.js +277 -0
  86. package/dist/task/spec-doc.d.ts +78 -0
  87. package/dist/task/spec-doc.js +205 -0
  88. package/dist/task/spec-model.d.ts +70 -0
  89. package/dist/task/spec-model.js +139 -0
  90. package/dist/task/stall-detector.d.ts +1 -1
  91. package/dist/task/stall-detector.js +9 -5
  92. package/dist/task/state-dir.d.ts +29 -0
  93. package/dist/task/state-dir.js +80 -0
  94. package/dist/task/suppression-probe.d.ts +67 -0
  95. package/dist/task/suppression-probe.js +78 -0
  96. package/dist/task/task-gates.d.ts +52 -58
  97. package/dist/task/task-gates.js +161 -199
  98. package/dist/task/task-io.d.ts +10 -0
  99. package/dist/task/task-io.js +69 -18
  100. package/dist/task/task-parsers.js +6 -6
  101. package/dist/task/task-types.d.ts +7 -0
  102. package/dist/task/timings.d.ts +11 -0
  103. package/dist/task/timings.js +21 -0
  104. package/dist/task/tree-hash.d.ts +8 -0
  105. package/dist/task/tree-hash.js +55 -0
  106. package/dist/task/verify-reconcile.d.ts +4 -3
  107. package/dist/task/verify-reconcile.js +7 -8
  108. package/dist/task/verify-resolution.d.ts +32 -6
  109. package/dist/task/verify-resolution.js +78 -15
  110. package/dist/task/verify-work.d.ts +105 -35
  111. package/dist/task/verify-work.js +232 -60
  112. package/dist/task/yolo.d.ts +0 -13
  113. package/dist/task/yolo.js +9 -21
  114. package/dist/workers/pi-worker-core.d.ts +20 -0
  115. package/dist/workers/pi-worker-core.js +16 -11
  116. package/dist/workers/pi-worker-docs.d.ts +6 -5
  117. package/dist/workers/pi-worker-docs.js +8 -7
  118. package/dist/workers/pi-worker-fetch.d.ts +3 -2
  119. package/dist/workers/pi-worker-fetch.js +5 -4
  120. package/dist/workers/pi-worker-search.js +5 -4
  121. package/dist/workers/research-cache.d.ts +23 -1
  122. package/dist/workers/research-cache.js +63 -3
  123. package/dist/workers/shared.d.ts +18 -0
  124. package/dist/workers/shared.js +38 -22
  125. 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;