@mjasnikovs/pi-task 0.40.50 → 0.41.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 +15 -1
- package/dist/task/accept-debt.js +4 -0
- package/dist/task/artifact-closure.js +7 -1
- package/dist/task/auto-io.d.ts +59 -7
- package/dist/task/auto-io.js +140 -47
- package/dist/task/auto-orchestrator.d.ts +22 -19
- package/dist/task/auto-orchestrator.js +126 -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 +199 -38
- 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/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 +39 -57
- package/dist/task/task-gates.js +115 -195
- 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 +28 -5
- package/dist/task/verify-resolution.js +70 -13
- package/dist/task/verify-work.d.ts +98 -35
- package/dist/task/verify-work.js +227 -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
package/dist/task/verify-work.js
CHANGED
|
@@ -42,6 +42,11 @@ import { buildEnvNotesBlock, ENV_NOTE_EMIT_INSTRUCTION, extractEnvNotes } from '
|
|
|
42
42
|
import { buildContractsVerifyBlock } from './contracts.js';
|
|
43
43
|
import { findSkipEscapes, skipEscapeVerifyFindings } from './skip-escape.js';
|
|
44
44
|
import { crossTaskDeletionVerifyFindings } from './task-provenance.js';
|
|
45
|
+
import { classifyHealthDelta, inheritedHealthFindings } from './health-baseline.js';
|
|
46
|
+
import { parseSpec, sliceSpecSection } from './spec-model.js';
|
|
47
|
+
import { qaKindsFromRecord } from './qa-transcript.js';
|
|
48
|
+
import { annotateConstraints, anyBinding, renderConstraintPolicy } from './constraint-policy.js';
|
|
49
|
+
import { suppressionVerifyFindings } from './suppression-probe.js';
|
|
45
50
|
/**
|
|
46
51
|
* The verification child gets exactly two tools: `read` and `bash`.
|
|
47
52
|
*
|
|
@@ -99,33 +104,12 @@ export function isStaticClass(cls) {
|
|
|
99
104
|
return cls === 'repo-health' || cls === 'static-checks';
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* `## ` plus non-space (`## phase timings`); a `### ` subheading stays inside.
|
|
106
|
-
* Returns null when there is no spec section or it is blank (a task that never
|
|
107
|
-
* reached compose), which the caller treats as a pass — nothing to verify against.
|
|
107
|
+
* The delivered spec's TEXT, for the children that must read its prose verbatim.
|
|
108
|
+
* The slicing itself lives in spec-model.ts beside the parser, so "the spec
|
|
109
|
+
* section" is one definition rather than one per consumer.
|
|
108
110
|
*/
|
|
109
111
|
export function extractSpecForVerification(taskBody) {
|
|
110
|
-
|
|
111
|
-
let start = -1;
|
|
112
|
-
for (let i = 0; i < lines.length; i++) {
|
|
113
|
-
if (/^##\s+spec\s*$/i.test(lines[i])) {
|
|
114
|
-
start = i + 1;
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (start === -1)
|
|
119
|
-
return null;
|
|
120
|
-
let end = lines.length;
|
|
121
|
-
for (let i = start; i < lines.length; i++) {
|
|
122
|
-
if (/^##\s+\S/.test(lines[i])) {
|
|
123
|
-
end = i;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const spec = lines.slice(start, end).join('\n').trim();
|
|
128
|
-
return spec.length > 0 ? spec : null;
|
|
112
|
+
return sliceSpecSection(taskBody);
|
|
129
113
|
}
|
|
130
114
|
/**
|
|
131
115
|
* Build one row. Generic in the probe's return type so `empty`/`findings` stay
|
|
@@ -146,8 +130,8 @@ function probeAdapter(row) {
|
|
|
146
130
|
block,
|
|
147
131
|
ruleId,
|
|
148
132
|
rule,
|
|
149
|
-
run: async (deps, onStage) => {
|
|
150
|
-
const probe = source(deps);
|
|
133
|
+
run: async (deps, onStage, pre) => {
|
|
134
|
+
const probe = source(deps, pre);
|
|
151
135
|
// Probes are INDEPENDENTLY OPTIONAL: an absent probe is "skipped", and a
|
|
152
136
|
// probe that throws degrades to its own empty value — it is a sharpener,
|
|
153
137
|
// never a blocker, so a fault in one can neither block the gate nor
|
|
@@ -171,6 +155,68 @@ const asLines = (raw) => raw;
|
|
|
171
155
|
/** The table. ROW ORDER IS THE NOTICE-BLOCK ORDER IN THE PROMPT: `buildVerifyPrompt`
|
|
172
156
|
* flatMaps this array to build the notices, so reordering rows reorders the prompt. */
|
|
173
157
|
const PROBE_ADAPTERS = [
|
|
158
|
+
/**
|
|
159
|
+
* The project's own check and build commands, ALREADY RUN by the parent against
|
|
160
|
+
* this exact tree (see gate-evidence.ts). The one row that supplies a RESULT
|
|
161
|
+
* rather than a warning, and the one row rule 1 switches on: a child handed the
|
|
162
|
+
* output spends its turn judging it instead of reproducing a suite the previous
|
|
163
|
+
* child already ran.
|
|
164
|
+
*/
|
|
165
|
+
probeAdapter({
|
|
166
|
+
key: 'evidence',
|
|
167
|
+
stage: 'project checks',
|
|
168
|
+
empty: [],
|
|
169
|
+
findings: asLines,
|
|
170
|
+
ruleId: '1',
|
|
171
|
+
block: findings => [
|
|
172
|
+
'EVIDENCE (deterministic, run by the orchestrator against THIS tree, just now):',
|
|
173
|
+
"the project's own check and build commands have ALREADY BEEN RUN for you. Each",
|
|
174
|
+
'line is the command, how it ended, and the file holding its complete output:',
|
|
175
|
+
...findings.map(f => `- ${f}`),
|
|
176
|
+
'Read those files with your `read` tool. A command listed here does not need',
|
|
177
|
+
'running again (rule 1) — re-run only a targeted subset: a single test file, or',
|
|
178
|
+
'one command whose reported result you have a concrete reason to doubt. SKIPPED',
|
|
179
|
+
'means the command could not run on this machine at all, so it is evidence of',
|
|
180
|
+
'nothing in either direction — rules 5 and 5c decide what its absence means.',
|
|
181
|
+
''
|
|
182
|
+
]
|
|
183
|
+
}),
|
|
184
|
+
/**
|
|
185
|
+
* PRE-EXISTING repo health (see health-baseline.ts): static checks that were
|
|
186
|
+
* ALREADY failing, the same way, before this task started. They used to be an
|
|
187
|
+
* absolute FAIL that short-circuited the whole pass, so the task answered for
|
|
188
|
+
* a sibling's defect and its own probe findings were never computed. As a row
|
|
189
|
+
* they are stated to the child instead: judge this task's work, and do not
|
|
190
|
+
* read a check that was red on arrival as evidence about it either way.
|
|
191
|
+
*/
|
|
192
|
+
probeAdapter({
|
|
193
|
+
key: 'repoHealth',
|
|
194
|
+
source: (_deps, pre) => () => Promise.resolve(pre.repoHealth),
|
|
195
|
+
empty: [],
|
|
196
|
+
findings: asLines,
|
|
197
|
+
ruleId: '4h',
|
|
198
|
+
block: findings => [
|
|
199
|
+
'INHERITED REPO-HEALTH NOTICE (deterministic, computed by the orchestrator by',
|
|
200
|
+
"re-running the project's own static checks and comparing them against the",
|
|
201
|
+
'baseline taken before this task started): these checks were ALREADY failing,',
|
|
202
|
+
'with the same exit code, before any of this work existed:',
|
|
203
|
+
...findings.map(f => `- ${f}`),
|
|
204
|
+
"They are NOT this task's defect and must not decide this verdict — rule 4h.",
|
|
205
|
+
'Equally, a red check that was red before proves nothing about this work: do',
|
|
206
|
+
'not read its output as evidence that the shipped behavior is correct.',
|
|
207
|
+
''
|
|
208
|
+
],
|
|
209
|
+
rule: [
|
|
210
|
+
"4h. AN INHERITED RED CHECK IS NOT THIS TASK'S FAIL, AND NOT ITS PROOF — when the",
|
|
211
|
+
' INHERITED REPO-HEALTH NOTICE above names a check, that check failed identically',
|
|
212
|
+
' before this task ran. Do NOT fail this work for it: the defect belongs to',
|
|
213
|
+
' whatever put it there, it is recorded as durable debt, and the run-end gate',
|
|
214
|
+
' re-checks it. Do NOT lean on it either — a command that was already exiting',
|
|
215
|
+
" non-zero tells you nothing about this task's behavior, so verify that behavior",
|
|
216
|
+
' another way. A check that is failing DIFFERENTLY, or one absent from the notice,',
|
|
217
|
+
" is this task's to answer for in the ordinary way."
|
|
218
|
+
]
|
|
219
|
+
}),
|
|
174
220
|
/**
|
|
175
221
|
* Deterministic self-verification probe (see substitution-probe.ts): test
|
|
176
222
|
* files this task itself authored or changed, with their added-line counts.
|
|
@@ -212,27 +258,33 @@ const PROBE_ADAPTERS = [
|
|
|
212
258
|
block: findings => [
|
|
213
259
|
'PROHIBITION NOTICE (deterministic, computed by the orchestrator from the',
|
|
214
260
|
"spec's own constraint lines and the task's diff): this task MODIFIED paths",
|
|
215
|
-
'the spec
|
|
261
|
+
'the spec forbids modifying, each tagged with the weight of the constraint',
|
|
262
|
+
'that forbids it:',
|
|
216
263
|
...findings.map(f => `- ${f}`),
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
...(anyBinding(findings) ?
|
|
265
|
+
[
|
|
266
|
+
'Read the exact constraint wording in the spec. Unless that wording itself',
|
|
267
|
+
'states an exception that covers this change, a BINDING one is a violated',
|
|
268
|
+
'prohibition: rule 4b applies and the verdict is FAIL naming the forbidden',
|
|
269
|
+
'path — even if every test passes and the change looks harmless.'
|
|
270
|
+
]
|
|
271
|
+
: [
|
|
272
|
+
'Every one of these is ADVISORY — automation, not a person, wrote the',
|
|
273
|
+
'constraint that forbids it (rule 4b). Report each in your findings, and do',
|
|
274
|
+
'NOT fail the work for it unless an ACCEPTANCE criterion also fails. The',
|
|
275
|
+
'acceptance criteria are the bar here.'
|
|
276
|
+
]),
|
|
221
277
|
''
|
|
222
278
|
],
|
|
223
279
|
rule: [
|
|
224
|
-
'4b.
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
'
|
|
232
|
-
' violation was fully REVERTED (the shipped tree no longer violates), or the',
|
|
233
|
-
' prohibition\'s own wording states an exception ("except…", "beyond what is needed',
|
|
234
|
-
' for…") that covers the change — judged against that stated exception, not against',
|
|
235
|
-
' your view of harmlessness.'
|
|
280
|
+
...renderConstraintPolicy('4b.'),
|
|
281
|
+
' Applying that to a PROHIBITION ("Do NOT modify X", "MUST NOT touch Y"): check the',
|
|
282
|
+
" task's own diff against the spec's prohibitions — a forbidden file can be",
|
|
283
|
+
' modified without any test noticing. For a BINDING one, two outcomes are not a',
|
|
284
|
+
' FAIL: the violation was fully REVERTED (the shipped tree no longer violates), or',
|
|
285
|
+
' the prohibition\'s own wording states an exception ("except…", "beyond what is',
|
|
286
|
+
' needed for…") that covers the change — judged against that stated exception, not',
|
|
287
|
+
' against your view of harmlessness.'
|
|
236
288
|
]
|
|
237
289
|
}),
|
|
238
290
|
/**
|
|
@@ -485,6 +537,43 @@ const PROBE_ADAPTERS = [
|
|
|
485
537
|
'fails where the test passes, report FAIL and name the wiring seam.',
|
|
486
538
|
''
|
|
487
539
|
]
|
|
540
|
+
}),
|
|
541
|
+
/**
|
|
542
|
+
* Deterministic suppression-widening probe (see suppression-probe.ts): checker
|
|
543
|
+
* suppressions this task NET added, per file. The child cannot find these by
|
|
544
|
+
* running the checks — a suppression's whole purpose is that the check goes
|
|
545
|
+
* quiet — so the count has to arrive as a finding (0034 shipped sixteen and
|
|
546
|
+
* every gate reported success).
|
|
547
|
+
*/
|
|
548
|
+
probeAdapter({
|
|
549
|
+
key: 'suppressionWidening',
|
|
550
|
+
stage: 'suppression probe',
|
|
551
|
+
empty: [],
|
|
552
|
+
findings: suppressionVerifyFindings,
|
|
553
|
+
ruleId: '4i',
|
|
554
|
+
block: findings => [
|
|
555
|
+
'SUPPRESSION WIDENING NOTICE (deterministic, computed by the orchestrator by',
|
|
556
|
+
"counting each suppression pattern's added minus removed lines in this task's",
|
|
557
|
+
'diff): this task added checker suppressions:',
|
|
558
|
+
...findings.map(f => `- ${f}`),
|
|
559
|
+
'A suppression makes a checker SILENT, not satisfied. You cannot discover this',
|
|
560
|
+
"by running the project's checks — they are green because of these lines. For",
|
|
561
|
+
'each, read the surrounding code and decide whether the underlying defect was',
|
|
562
|
+
'FIXED or merely silenced, and whether the spec asked for the suppression at',
|
|
563
|
+
'all (rule 4i).',
|
|
564
|
+
''
|
|
565
|
+
],
|
|
566
|
+
rule: [
|
|
567
|
+
'4i. A SILENCED CHECKER IS NOT A SATISFIED ONE — when the SUPPRESSION WIDENING',
|
|
568
|
+
' NOTICE above names added suppressions (`@ts-expect-error`, `eslint-disable`,',
|
|
569
|
+
' `# noqa`, `//nolint`, `#[allow(…)]`, `as unknown as` …), the green check they',
|
|
570
|
+
' produce is not evidence about the code under them. Judge each one: a',
|
|
571
|
+
' suppression the spec explicitly asked for, or one narrowly scoped to a',
|
|
572
|
+
' documented third-party defect the task cannot fix, is fine and you say so. A',
|
|
573
|
+
' suppression that exists so a checker would stop reporting a defect this task',
|
|
574
|
+
' introduced is that defect, still shipped — verdict FAIL naming the file and the',
|
|
575
|
+
' finding it hides. A blanket file-level disable is never the narrow case.'
|
|
576
|
+
]
|
|
488
577
|
})
|
|
489
578
|
];
|
|
490
579
|
/**
|
|
@@ -493,6 +582,43 @@ const PROBE_ADAPTERS = [
|
|
|
493
582
|
* checked against the table rather than against a hand-kept list.
|
|
494
583
|
*/
|
|
495
584
|
export const BOUND_PROBE_KEYS = PROBE_ADAPTERS.filter(a => a.bound).map(a => a.key);
|
|
585
|
+
/**
|
|
586
|
+
* Rule 1 in its two states, picked by whether the evidence row supplied anything.
|
|
587
|
+
*
|
|
588
|
+
* A child told "run the project's own commands" directly under a block listing
|
|
589
|
+
* those commands' results runs them again — the instruction and the notice
|
|
590
|
+
* contradict each other, and the instruction is the numbered one. So the rule
|
|
591
|
+
* moves with the evidence rather than sitting beside it. Without evidence (no
|
|
592
|
+
* verified tooling, or a caller that binds no probes) the original obligation is
|
|
593
|
+
* the only correct one.
|
|
594
|
+
*/
|
|
595
|
+
const RULE_1_RUN_THEM = [
|
|
596
|
+
"1. Run the project's OWN commands — the verbatim scripts / targets / binaries it",
|
|
597
|
+
' ships (package.json scripts, Makefile targets, the command the spec names) —',
|
|
598
|
+
' exactly as written, in the workspace as you found it. Judge the artifact or',
|
|
599
|
+
" output they actually PRODUCE. The project's own command and its real output are",
|
|
600
|
+
" the bar; also run the spec's VERIFY block, but it does not override that bar."
|
|
601
|
+
];
|
|
602
|
+
const RULE_1_ALREADY_RUN = [
|
|
603
|
+
"1. The project's OWN check and build commands have ALREADY BEEN RUN against this",
|
|
604
|
+
' exact tree, and the EVIDENCE block above names each one, its real exit code and',
|
|
605
|
+
' the file holding its complete output. READ THOSE FILES — that output is the bar,',
|
|
606
|
+
' and a command reported there does not need running again. Re-run only a TARGETED',
|
|
607
|
+
' SUBSET: a single test file, or one command whose reported result you have a',
|
|
608
|
+
' concrete reason to doubt (its output contradicts itself, or it never ran).',
|
|
609
|
+
" Judge the artifact or output the commands actually PRODUCED. Also run the spec's",
|
|
610
|
+
' VERIFY block, and any command the evidence does not cover, but neither overrides',
|
|
611
|
+
' that bar.'
|
|
612
|
+
];
|
|
613
|
+
/**
|
|
614
|
+
* The spec as a VALUE, with each constraint's `[from:]` tag resolved against the
|
|
615
|
+
* task's own Q&A record. Shared by the verify prompt and the resolution judge, so
|
|
616
|
+
* one constraint cannot carry two weights.
|
|
617
|
+
*/
|
|
618
|
+
export function specValue(spec, qaRecord) {
|
|
619
|
+
const kinds = qaRecord === undefined ? [] : qaKindsFromRecord(qaRecord);
|
|
620
|
+
return parseSpec(spec, n => kinds[n - 1] ?? null);
|
|
621
|
+
}
|
|
496
622
|
/**
|
|
497
623
|
* Build the verification child's prompt. Kept pure so the wording is unit-tested
|
|
498
624
|
* without spawning pi. The contract: run the spec's own verification in the real
|
|
@@ -503,7 +629,7 @@ export const BOUND_PROBE_KEYS = PROBE_ADAPTERS.filter(a => a.bound).map(a => a.k
|
|
|
503
629
|
* block — the probes are independently optional.
|
|
504
630
|
*/
|
|
505
631
|
export function buildVerifyPrompt(spec, findings = {}, context = {}) {
|
|
506
|
-
const { envNotes, contracts } = context;
|
|
632
|
+
const { envNotes, envRunId, contracts } = context;
|
|
507
633
|
// NOTICE BLOCKS in table order; RULES sorted by their hand-assigned number.
|
|
508
634
|
const noticeBlocks = PROBE_ADAPTERS.flatMap(adapter => {
|
|
509
635
|
const lines = findings[adapter.key];
|
|
@@ -514,8 +640,16 @@ export function buildVerifyPrompt(spec, findings = {}, context = {}) {
|
|
|
514
640
|
: a.ruleId > b.ruleId ? 1
|
|
515
641
|
: 0)
|
|
516
642
|
.flatMap(a => [...(a.rule ?? []), '']);
|
|
517
|
-
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes)] : [];
|
|
643
|
+
const envBlock = envNotes && envNotes.trim().length > 0 ? [buildEnvNotesBlock(envNotes, envRunId)] : [];
|
|
518
644
|
const contractsBlock = contracts && contracts.trim().length > 0 ? [buildContractsVerifyBlock(contracts)] : [];
|
|
645
|
+
const weights = annotateConstraints(specValue(spec, context.qaRecord).constraints);
|
|
646
|
+
const weightsBlock = weights === null ?
|
|
647
|
+
[]
|
|
648
|
+
: [
|
|
649
|
+
"THE SPEC'S CONSTRAINTS, BY WEIGHT (rule 4b says what each weight is worth):",
|
|
650
|
+
weights,
|
|
651
|
+
''
|
|
652
|
+
];
|
|
519
653
|
return [
|
|
520
654
|
'You are a strict verification pass running right after an AI coding agent',
|
|
521
655
|
'finished a task and committed it. The agent is known to mark work "done"',
|
|
@@ -531,15 +665,12 @@ export function buildVerifyPrompt(spec, findings = {}, context = {}) {
|
|
|
531
665
|
'',
|
|
532
666
|
...envBlock,
|
|
533
667
|
...contractsBlock,
|
|
668
|
+
...weightsBlock,
|
|
534
669
|
...noticeBlocks,
|
|
535
670
|
'How to verify — verify the REAL, shipped deliverable exactly as an unaided fresh',
|
|
536
671
|
'checkout (or CI run) would experience it:',
|
|
537
672
|
'',
|
|
538
|
-
|
|
539
|
-
' ships (package.json scripts, Makefile targets, the command the spec names) —',
|
|
540
|
-
' exactly as written, in the workspace as you found it. Judge the artifact or',
|
|
541
|
-
" output they actually PRODUCE. The project's own command and its real output are",
|
|
542
|
-
" the bar; also run the spec's VERIFY block, but it does not override that bar.",
|
|
673
|
+
...((findings.evidence?.length ?? 0) > 0 ? RULE_1_ALREADY_RUN : RULE_1_RUN_THEM),
|
|
543
674
|
'',
|
|
544
675
|
'2. Do NOT prepare, repair, reconfigure, or stand in for the run to make a check',
|
|
545
676
|
' pass. Concretely, to reach a green result you must NOT: set or export an',
|
|
@@ -764,15 +895,28 @@ export async function runWorkVerification(deps) {
|
|
|
764
895
|
// progress reporting must never break the gate
|
|
765
896
|
}
|
|
766
897
|
};
|
|
898
|
+
// DIFFERENTIAL, not absolute. A red check is this task's FAIL only when the
|
|
899
|
+
// baseline says it was not already red the same way; otherwise it is stated to
|
|
900
|
+
// the child as a probe row and recorded as inherited debt, and the pass
|
|
901
|
+
// continues — the short-circuit is what used to throw away every deterministic
|
|
902
|
+
// finding about the task's OWN work.
|
|
903
|
+
const pre = { repoHealth: [] };
|
|
904
|
+
let inheritedHealth;
|
|
767
905
|
if (deps.repoHealth) {
|
|
768
906
|
stage('repo health');
|
|
769
907
|
const h = await deps.repoHealth();
|
|
770
908
|
if (!h.ok) {
|
|
771
|
-
|
|
909
|
+
const baseline = deps.healthBaseline ? await deps.healthBaseline() : null;
|
|
910
|
+
if (classifyHealthDelta(baseline?.outcome ?? null, h) === 'regressed') {
|
|
911
|
+
return { ok: false, failClass: 'repo-health', reason: `repo health: ${h.reason}` };
|
|
912
|
+
}
|
|
913
|
+
pre.repoHealth = inheritedHealthFindings(h);
|
|
914
|
+
inheritedHealth = `repo health: ${h.reason} — already failing before this task`;
|
|
772
915
|
}
|
|
773
916
|
}
|
|
917
|
+
const inherited = inheritedHealth === undefined ? {} : { inheritedHealth };
|
|
774
918
|
if (!deps.spec || deps.spec.trim().length === 0) {
|
|
775
|
-
return { ok: true, reason: 'no spec to verify' };
|
|
919
|
+
return { ok: true, reason: 'no spec to verify', ...inherited };
|
|
776
920
|
}
|
|
777
921
|
// Every deterministic probe, one table row each (see PROBE_ADAPTERS): the row
|
|
778
922
|
// knows which dep it reads, what it degrades to, and which notice block its
|
|
@@ -781,10 +925,17 @@ export async function runWorkVerification(deps) {
|
|
|
781
925
|
const findings = {};
|
|
782
926
|
const rawResults = new Map();
|
|
783
927
|
for (const adapter of PROBE_ADAPTERS) {
|
|
784
|
-
const result = await adapter.run(deps, stage);
|
|
928
|
+
const result = await adapter.run(deps, stage, pre);
|
|
785
929
|
findings[adapter.key] = result.findings;
|
|
786
930
|
rawResults.set(adapter.key, result.raw);
|
|
787
931
|
}
|
|
932
|
+
// Everything the gate now knows deterministically, carried on the outcome so
|
|
933
|
+
// an AUTOFIX re-run is TOLD it (fix-context.ts) rather than re-deriving it —
|
|
934
|
+
// which is what a re-run told only "lint fails" does by widening suppressions.
|
|
935
|
+
// Only channels that FOUND something: a bag of empty arrays is not knowledge,
|
|
936
|
+
// and every consumer would have to filter it back out.
|
|
937
|
+
const found = Object.fromEntries(Object.entries(findings).filter(([, lines]) => lines.length > 0));
|
|
938
|
+
const probed = Object.keys(found).length === 0 ? {} : { probes: found };
|
|
788
939
|
// The one probe whose RAW value is needed beyond the prompt: cross-task deletion
|
|
789
940
|
// findings ride on a FAIL outcome (structured) so an ACCEPT can record them as
|
|
790
941
|
// durable debts. The row's `empty` is `[]`, so this is always an array.
|
|
@@ -818,7 +969,12 @@ export async function runWorkVerification(deps) {
|
|
|
818
969
|
for (let attempt = 1;; attempt++) {
|
|
819
970
|
let text;
|
|
820
971
|
try {
|
|
821
|
-
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, {
|
|
972
|
+
text = await deps.runChild(VERIFY_TOOLS, buildVerifyPrompt(deps.spec, findings, {
|
|
973
|
+
envNotes,
|
|
974
|
+
...(deps.qaRecord === undefined ? {} : { qaRecord: deps.qaRecord }),
|
|
975
|
+
...(deps.envNotes?.runId === undefined ? {} : { envRunId: deps.envNotes.runId }),
|
|
976
|
+
contracts
|
|
977
|
+
}), deps.signal);
|
|
822
978
|
}
|
|
823
979
|
catch (err) {
|
|
824
980
|
if (err instanceof Error && err.message === USER_CANCELLED)
|
|
@@ -830,7 +986,9 @@ export async function runWorkVerification(deps) {
|
|
|
830
986
|
return {
|
|
831
987
|
ok: false,
|
|
832
988
|
failClass: 'harness-fault',
|
|
833
|
-
reason: `${VERIFY_FAIL_PREFIX['harness-fault']} ${msg}
|
|
989
|
+
reason: `${VERIFY_FAIL_PREFIX['harness-fault']} ${msg}`,
|
|
990
|
+
...inherited,
|
|
991
|
+
...probed
|
|
834
992
|
};
|
|
835
993
|
}
|
|
836
994
|
// Capture the environment facts the child shared — regardless of verdict
|
|
@@ -853,13 +1011,18 @@ export async function runWorkVerification(deps) {
|
|
|
853
1011
|
continue;
|
|
854
1012
|
return {
|
|
855
1013
|
ok: false,
|
|
1014
|
+
// The child judged a tree it had itself changed — the FAULT is the
|
|
1015
|
+
// pass, not the work, so no implementation re-run can move it.
|
|
1016
|
+
failClass: 'harness-fault',
|
|
856
1017
|
reason: 'verify child mutated repo state and its verdict was discarded '
|
|
857
|
-
+ `(state restored: ${mutation.detail.slice(0, 200)})
|
|
1018
|
+
+ `(state restored: ${mutation.detail.slice(0, 200)})`,
|
|
1019
|
+
...inherited,
|
|
1020
|
+
...probed
|
|
858
1021
|
};
|
|
859
1022
|
}
|
|
860
1023
|
const verdict = parseVerifyVerdict(text);
|
|
861
1024
|
if (verdict.pass)
|
|
862
|
-
return { ok: true };
|
|
1025
|
+
return { ok: true, ...inherited, ...probed };
|
|
863
1026
|
if (verdict.detail === 'no verdict emitted' && attempt === 1)
|
|
864
1027
|
continue;
|
|
865
1028
|
// Structured cross-task deletion findings ride on every FAIL outcome: if the
|
|
@@ -875,14 +1038,18 @@ export async function runWorkVerification(deps) {
|
|
|
875
1038
|
unobserved: true,
|
|
876
1039
|
failClass: 'unobserved',
|
|
877
1040
|
reason: `work unobserved: ${verdict.detail}`,
|
|
878
|
-
...deletions
|
|
1041
|
+
...deletions,
|
|
1042
|
+
...inherited,
|
|
1043
|
+
...probed
|
|
879
1044
|
};
|
|
880
1045
|
}
|
|
881
1046
|
return {
|
|
882
1047
|
ok: false,
|
|
883
1048
|
failClass: 'model-verdict',
|
|
884
1049
|
reason: `work did not verify: ${verdict.detail}${verdict.detail === 'no verdict emitted' ? ' (after verify retry)' : ''}`,
|
|
885
|
-
...deletions
|
|
1050
|
+
...deletions,
|
|
1051
|
+
...inherited,
|
|
1052
|
+
...probed
|
|
886
1053
|
};
|
|
887
1054
|
}
|
|
888
1055
|
}
|
package/dist/task/yolo.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AutoAnswer } from './parsers.js';
|
|
2
|
-
import type { ResolutionChoice } from './verify-resolution.js';
|
|
3
2
|
import type { FinalGateChoice } from './final-gate-fix.js';
|
|
4
3
|
/**
|
|
5
4
|
* Visible provenance marker on the artifacts an auto-pick writes: gate trail lines
|
|
@@ -52,18 +51,6 @@ export declare function yoloPickAnswer(enabled: boolean, opts: {
|
|
|
52
51
|
* pattern-matching the answer text.
|
|
53
52
|
*/
|
|
54
53
|
export declare function yoloPickAutoAnswer(enabled: boolean, auto: AutoAnswer): YoloPick;
|
|
55
|
-
/**
|
|
56
|
-
* The verify-FAIL picker policy: ACCEPT (and write a debt), never AUTOFIX.
|
|
57
|
-
*
|
|
58
|
-
* Not a preference — a bound. task-gates.ts consults this only after its own
|
|
59
|
-
* unattended paths are exhausted: `autoFixNow` has run AUTOFIX up to
|
|
60
|
-
* MAX_AUTO_AUTOFIX times while the research recommended it, and the YOLO rescue has
|
|
61
|
-
* spent its one attempt on an ACCEPT recommendation with an untouched budget.
|
|
62
|
-
* Answering AUTOFIX here would restart that budget from the very site that proves
|
|
63
|
-
* it ran out. So YOLO takes the terminal option and records the defect under the
|
|
64
|
-
* `'yolo-accepted'` debt origin, which a human decision never produces.
|
|
65
|
-
*/
|
|
66
|
-
export declare function yoloVerifyResolution(enabled: boolean): ResolutionChoice | null;
|
|
67
54
|
/**
|
|
68
55
|
* The final-integration-gate policy: keep autofixing WHILE the picker still offers
|
|
69
56
|
* that card — run-final-gate.ts withdraws it from `options` after
|
package/dist/task/yolo.js
CHANGED
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* WHY PER-SITE, NOT ONE HOOK (the trap this module exists to avoid): the single
|
|
10
10
|
* interactive choke point is SessionUI.ask() (remote/bridge.ts) — auto-picking
|
|
11
11
|
* "index 0" inside it would be one tiny patch, and it would be wrong. One way the
|
|
12
|
-
* verify-FAIL picker is reached is after
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* verify-FAIL picker is reached is after every unattended autofix attempt has
|
|
13
|
+
* failed, and it STILL tints AUTOFIX as recommended, because the research still
|
|
14
|
+
* recommends it. A central hook would pick AUTOFIX forever and defeat the exact
|
|
15
|
+
* cap that exists to break a non-converging loop. So every site decides for
|
|
16
|
+
* itself, BEFORE ask() is called — which also means the prompt notification
|
|
17
|
+
* (bridge.ts holds exactly one pushNotify, inside ask()) is suppressed
|
|
18
|
+
* structurally, with zero suppression code. The verify gate's own site is
|
|
19
|
+
* gate-resolution.ts's decision table, which reads {@link isYoloMode} as its
|
|
20
|
+
* `unattended` input.
|
|
19
21
|
*
|
|
20
22
|
* Guard direction (repo constraint): an auto-pick may cost time, never work.
|
|
21
23
|
* Anything this module cannot stand behind — no recommendation to take, an answer
|
|
@@ -85,20 +87,6 @@ export function yoloPickAutoAnswer(enabled, auto) {
|
|
|
85
87
|
})
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The verify-FAIL picker policy: ACCEPT (and write a debt), never AUTOFIX.
|
|
90
|
-
*
|
|
91
|
-
* Not a preference — a bound. task-gates.ts consults this only after its own
|
|
92
|
-
* unattended paths are exhausted: `autoFixNow` has run AUTOFIX up to
|
|
93
|
-
* MAX_AUTO_AUTOFIX times while the research recommended it, and the YOLO rescue has
|
|
94
|
-
* spent its one attempt on an ACCEPT recommendation with an untouched budget.
|
|
95
|
-
* Answering AUTOFIX here would restart that budget from the very site that proves
|
|
96
|
-
* it ran out. So YOLO takes the terminal option and records the defect under the
|
|
97
|
-
* `'yolo-accepted'` debt origin, which a human decision never produces.
|
|
98
|
-
*/
|
|
99
|
-
export function yoloVerifyResolution(enabled) {
|
|
100
|
-
return enabled ? { action: 'accept' } : null;
|
|
101
|
-
}
|
|
102
90
|
/**
|
|
103
91
|
* The final-integration-gate policy: keep autofixing WHILE the picker still offers
|
|
104
92
|
* that card — run-final-gate.ts withdraws it from `options` after
|
|
@@ -399,6 +399,12 @@ export interface RunWorkerResult {
|
|
|
399
399
|
*/
|
|
400
400
|
interface RestartState {
|
|
401
401
|
loopHit?: LoopHit;
|
|
402
|
+
/**
|
|
403
|
+
* The killed attempt's read-set (loop-detector.ts). A runaway kill discards
|
|
404
|
+
* every byte the child read and nothing else records what that was, so it is
|
|
405
|
+
* what the loop and stall hints carry in place of answer text.
|
|
406
|
+
*/
|
|
407
|
+
visited: readonly string[];
|
|
402
408
|
commandKill?: CommandKill;
|
|
403
409
|
streamStalled?: {
|
|
404
410
|
idleMs: number;
|
|
@@ -462,5 +468,19 @@ interface RestartRule {
|
|
|
462
468
|
/** Backoff before re-spawning, in ms. Only the connection rule waits. */
|
|
463
469
|
backoffMs?: (s: RestartState) => number;
|
|
464
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* The restart ladder, in precedence order. FIRST MATCH WINS.
|
|
473
|
+
*
|
|
474
|
+
* Read the `!loopHit` guards as "a loop kill outranks me even when it has no
|
|
475
|
+
* budget left". They are not redundant with row order: when a loop is detected
|
|
476
|
+
* but the shared budget is spent, row 1 declines, and without those guards row 2
|
|
477
|
+
* or 4 would then restart the same runaway child under a hint that does not
|
|
478
|
+
* describe why it died.
|
|
479
|
+
*
|
|
480
|
+
* The whole ritual — check the budget, set the hint, spend the counters, record
|
|
481
|
+
* and announce the discarded attempt, sleep, re-spawn — belongs to the loop in
|
|
482
|
+
* `runWorker`, so a new failure mode is one row here and cannot be added without
|
|
483
|
+
* becoming visible in `restarts`.
|
|
484
|
+
*/
|
|
465
485
|
export declare const RESTART_RULES: readonly RestartRule[];
|
|
466
486
|
export declare function runWorker(input: RunWorkerInput): Promise<RunWorkerResult>;
|
|
@@ -102,7 +102,9 @@ const CARRY_FORWARD_LIMIT = 24_000;
|
|
|
102
102
|
* `worker-timeout` and `connection-error` all discard work the model genuinely
|
|
103
103
|
* did. A loop kill and a leaked tool call do not — the first is by definition the
|
|
104
104
|
* same call repeated, the second is malformed protocol text, and replaying either
|
|
105
|
-
* would feed the failure back to itself.
|
|
105
|
+
* would feed the failure back to itself. A loop kill also fires from the tool-call
|
|
106
|
+
* hook, before any answer text streams, so there is nothing to carry; what it
|
|
107
|
+
* hands the re-spawn is the READ-SET instead (`RestartState.visited`).
|
|
106
108
|
*/
|
|
107
109
|
const CARRY_FORWARD_REASONS = CARRY_FORWARD_IDS;
|
|
108
110
|
/**
|
|
@@ -245,6 +247,15 @@ absoluteCeilingMs) {
|
|
|
245
247
|
}
|
|
246
248
|
};
|
|
247
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* A stall hit counts a streak or a byte total, not calls in a window, so printing
|
|
252
|
+
* the loop shape would misname why the attempt died.
|
|
253
|
+
*/
|
|
254
|
+
function loopDetail(hit) {
|
|
255
|
+
if (hit.stall)
|
|
256
|
+
return `${hit.call.name} ${hit.stall} ×${hit.count}`;
|
|
257
|
+
return `${hit.call.name} ×${hit.count}${hit.windowSize === undefined ? '' : `/${hit.windowSize}`}`;
|
|
258
|
+
}
|
|
248
259
|
/**
|
|
249
260
|
* The restart ladder, in precedence order. FIRST MATCH WINS.
|
|
250
261
|
*
|
|
@@ -259,15 +270,6 @@ absoluteCeilingMs) {
|
|
|
259
270
|
* `runWorker`, so a new failure mode is one row here and cannot be added without
|
|
260
271
|
* becoming visible in `restarts`.
|
|
261
272
|
*/
|
|
262
|
-
/**
|
|
263
|
-
* A stall hit carries no meaningful windowSize (rule 1 sets it to 0), so
|
|
264
|
-
* printing the loop shape would misname why the attempt died.
|
|
265
|
-
*/
|
|
266
|
-
function loopDetail(hit) {
|
|
267
|
-
return hit.stall ?
|
|
268
|
-
`${hit.call.name} ${hit.stall} ×${hit.count}`
|
|
269
|
-
: `${hit.call.name} ×${hit.count}/${hit.windowSize}`;
|
|
270
|
-
}
|
|
271
273
|
export const RESTART_RULES = [
|
|
272
274
|
{
|
|
273
275
|
// A loop-kill is restarted with a hint naming the offending call so the
|
|
@@ -276,7 +278,9 @@ export const RESTART_RULES = [
|
|
|
276
278
|
detect: s => s.loopHit && s.restartBudgetSpent < MAX_LOOP_RESTARTS ?
|
|
277
279
|
{ detail: loopDetail(s.loopHit) }
|
|
278
280
|
: null,
|
|
279
|
-
hint: s => s.loopHit.stall ?
|
|
281
|
+
hint: s => s.loopHit.stall ?
|
|
282
|
+
formatStallHint(s.loopHit.stall, s.visited)
|
|
283
|
+
: formatLoopHint(s.loopHit, s.visited),
|
|
280
284
|
counters: { shared: true }
|
|
281
285
|
},
|
|
282
286
|
{
|
|
@@ -599,6 +603,7 @@ export async function runWorker(input) {
|
|
|
599
603
|
// cancel: a user who pressed ESC between attempts must not buy a spawn.
|
|
600
604
|
const state = {
|
|
601
605
|
...(loopHit ? { loopHit } : {}),
|
|
606
|
+
visited: loopDetector?.visited() ?? [],
|
|
602
607
|
...(commandKill ? { commandKill } : {}),
|
|
603
608
|
...(streamStalled ? { streamStalled } : {}),
|
|
604
609
|
stalled,
|
|
@@ -96,12 +96,13 @@ export declare function excerptFabricated(check: {
|
|
|
96
96
|
* the test imports the rule it is checking.
|
|
97
97
|
*/
|
|
98
98
|
export declare function docsCacheable(d: Pick<DocsDetails, 'typeOnly' | 'excerptVerified' | 'excerptFabricated' | 'abstained'>): boolean;
|
|
99
|
-
/** The docs cache key: a package's answer is per (module, question),
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
99
|
+
/** The docs cache key: a package's answer is per (module, question), the question
|
|
100
|
+
* reduced to its sorted distinctive tokens so phrasing variants share one entry and a
|
|
101
|
+
* narrower question can be answered from a wider one's digest (research-cache.ts).
|
|
102
|
+
* Returns null for the project-source `.` lookup, which is never cached — the working
|
|
103
|
+
* tree mutates as tasks implement.
|
|
103
104
|
*
|
|
104
|
-
* The ecosystem joins the key only when the caller named one, so the
|
|
105
|
+
* The ecosystem joins the key only when the caller named one, so the subjects of every
|
|
105
106
|
* call that lets the manifest decide are the ones they always were. */
|
|
106
107
|
export declare function docsCacheKey(params: {
|
|
107
108
|
module: string;
|
|
@@ -13,7 +13,7 @@ import { npmVersionLookup as defaultNpmVersionLookup, formatNpmVersionSection }
|
|
|
13
13
|
import { childFailureReason, makeWorkerTool, workerAnswer, workerUnavailable } from './shared.js';
|
|
14
14
|
import { isTypeOnlyAnswer } from '../task/type-only-answer.js';
|
|
15
15
|
import { logDocsAnswer } from './typeonly-log.js';
|
|
16
|
-
import { normalizeQuery } from './research-cache.js';
|
|
16
|
+
import { normalizeQuery, queryTokenKey } from './research-cache.js';
|
|
17
17
|
import { projectDocsRaw } from './docs-project.js';
|
|
18
18
|
import { projectDocsBudget, projectDocsBudgetExhausted } from '../task/research-fanout-budget.js';
|
|
19
19
|
import { isAbstention } from './abstention.js';
|
|
@@ -433,18 +433,19 @@ export function docsCacheable(d) {
|
|
|
433
433
|
// real answer and memoised the dead end for the whole run.
|
|
434
434
|
return d.typeOnly !== true && d.excerptFabricated !== true && d.abstained !== true;
|
|
435
435
|
}
|
|
436
|
-
/** The docs cache key: a package's answer is per (module, question),
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
436
|
+
/** The docs cache key: a package's answer is per (module, question), the question
|
|
437
|
+
* reduced to its sorted distinctive tokens so phrasing variants share one entry and a
|
|
438
|
+
* narrower question can be answered from a wider one's digest (research-cache.ts).
|
|
439
|
+
* Returns null for the project-source `.` lookup, which is never cached — the working
|
|
440
|
+
* tree mutates as tasks implement.
|
|
440
441
|
*
|
|
441
|
-
* The ecosystem joins the key only when the caller named one, so the
|
|
442
|
+
* The ecosystem joins the key only when the caller named one, so the subjects of every
|
|
442
443
|
* call that lets the manifest decide are the ones they always were. */
|
|
443
444
|
export function docsCacheKey(params) {
|
|
444
445
|
if (params.module === '.')
|
|
445
446
|
return null;
|
|
446
447
|
const scope = params.ecosystem ? `${params.ecosystem}::` : '';
|
|
447
|
-
return `${scope}${normalizeQuery(params.module)}
|
|
448
|
+
return `${scope}${normalizeQuery(params.module)}\u0000${queryTokenKey(params.query)}`;
|
|
448
449
|
}
|
|
449
450
|
/** Package provenance for per-entry resume invalidation: the package ROOT of the
|
|
450
451
|
* specifier (`hono/client` → `hono`), and undefined for the project-source `.`.
|