@mjasnikovs/pi-task 0.18.15 → 0.18.16

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 (55) hide show
  1. package/README.md +2 -2
  2. package/dist/task/accept-debt.d.ts +28 -1
  3. package/dist/task/accept-debt.js +61 -3
  4. package/dist/task/auto-io.d.ts +4 -2
  5. package/dist/task/auto-io.js +6 -3
  6. package/dist/task/auto-orchestrator.d.ts +1 -0
  7. package/dist/task/auto-orchestrator.js +135 -19
  8. package/dist/task/auto-prompts.d.ts +5 -5
  9. package/dist/task/auto-prompts.js +9 -2
  10. package/dist/task/contracts.d.ts +8 -0
  11. package/dist/task/contracts.js +4 -2
  12. package/dist/task/decompose-fidelity.d.ts +47 -0
  13. package/dist/task/decompose-fidelity.js +132 -0
  14. package/dist/task/final-gate-fix.d.ts +22 -3
  15. package/dist/task/final-gate-fix.js +72 -7
  16. package/dist/task/final-gate.d.ts +48 -1
  17. package/dist/task/final-gate.js +182 -34
  18. package/dist/task/gate-deps.d.ts +7 -0
  19. package/dist/task/gate-deps.js +37 -1
  20. package/dist/task/launch-contract.d.ts +36 -1
  21. package/dist/task/launch-contract.js +80 -2
  22. package/dist/task/phases.d.ts +13 -1
  23. package/dist/task/phases.js +50 -11
  24. package/dist/task/prompts.js +2 -0
  25. package/dist/task/render-check.d.ts +32 -0
  26. package/dist/task/render-check.js +186 -0
  27. package/dist/task/requirements.d.ts +88 -0
  28. package/dist/task/requirements.js +331 -0
  29. package/dist/task/verify-reconcile.d.ts +36 -0
  30. package/dist/task/verify-reconcile.js +203 -0
  31. package/dist/task/write-guard.d.ts +52 -0
  32. package/dist/task/write-guard.js +112 -0
  33. package/package.json +1 -1
  34. package/dist/task/_ab.d.ts +0 -1
  35. package/dist/task/_ab.js +0 -68
  36. package/dist/task/task-file.d.ts +0 -14
  37. package/dist/task/task-file.js +0 -15
  38. package/dist/think-test/cli.d.ts +0 -1
  39. package/dist/think-test/cli.js +0 -98
  40. package/dist/think-test/client.d.ts +0 -26
  41. package/dist/think-test/client.js +0 -37
  42. package/dist/think-test/compressor.d.ts +0 -5
  43. package/dist/think-test/compressor.js +0 -25
  44. package/dist/think-test/judge.d.ts +0 -4
  45. package/dist/think-test/judge.js +0 -11
  46. package/dist/think-test/score.d.ts +0 -8
  47. package/dist/think-test/score.js +0 -22
  48. package/dist/think-test/serialize.d.ts +0 -19
  49. package/dist/think-test/serialize.js +0 -41
  50. package/dist/think-test/transcript.d.ts +0 -7
  51. package/dist/think-test/transcript.js +0 -41
  52. package/dist/think-test/transform.d.ts +0 -6
  53. package/dist/think-test/transform.js +0 -24
  54. package/dist/think-test/types.d.ts +0 -45
  55. package/dist/think-test/types.js +0 -1
@@ -28,7 +28,10 @@ import { compressTitle } from './title-label.js';
28
28
  import { parseVerifyBlock, validateSpecShape, stripSpecPreamble, isCritiqueClean } from './spec-validation.js';
29
29
  import { findSkipEscapes, skipEscapeDefectText } from './skip-escape.js';
30
30
  import { findSynthesizedWiring, wiringProbeText, readReferencedDocs } from './wiring-claims.js';
31
+ import { findAbsenceConflicts, absenceProbeText, siblingTitlesFromPlanContext } from './verify-reconcile.js';
32
+ import { existsSync } from 'node:fs';
31
33
  import { readContracts, buildContractsBlock, buildContractsVerifyBlock } from './contracts.js';
34
+ import { readRequirements, buildRequirementsBlock } from './requirements.js';
32
35
  import { runPhaseChild, runPhaseWithLoopGuard, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
33
36
  import { SessionUI } from '../remote/bridge.js';
34
37
  // ─── Re-export constants from their home modules ────────────────────────────
@@ -128,9 +131,25 @@ export async function phaseContractsBlock(deps) {
128
131
  const contracts = await readContracts(deps.cwd).catch(() => '');
129
132
  return buildContractsBlock(contracts);
130
133
  }
134
+ /**
135
+ * The carried-context blocks a GENERATIVE phase (refine, compose) receives: the
136
+ * cross-slice contracts plus the carried cross-cutting requirements (mx5 run 11,
137
+ * goals A/C — `.pi-tasks/requirements.md`, written at plan time). The verbatim
138
+ * requirement quotes travel INTO every task's spec generation, so a mandated
139
+ * methodology ("a test lands in the same change as each new route") reaches the
140
+ * task's GOAL/CONSTRAINTS and its VERIFY — a pointer back to the spec doc
141
+ * recovered the dropped §10 in only 1 of ~6 applicable run-11 tasks; content
142
+ * travels, pointers don't. Both blocks are '' outside their runs, so a bare
143
+ * /task is byte-identical to before.
144
+ */
145
+ export async function phaseCarriedBlocks(deps) {
146
+ const contracts = await phaseContractsBlock(deps);
147
+ const requirements = buildRequirementsBlock(await readRequirements(deps.cwd).catch(() => ''));
148
+ return [contracts, requirements].filter(b => b.length > 0).join('\n');
149
+ }
131
150
  export const phaseRefine = async (deps, raw, planContext) => {
132
151
  const existingFiles = await refineExistingFilesBlock(deps).catch(() => '');
133
- const contracts = await phaseContractsBlock(deps);
152
+ const contracts = await phaseCarriedBlocks(deps);
134
153
  // Imperative tool directives the user wrote into the RAW prompt ("via web
135
154
  // search", "fetch <url>"). Refine paraphrases the task and a weak model drops
136
155
  // these some of the time (mx5 run 9: "via web search" vanished, the whole run
@@ -750,7 +769,7 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
750
769
  return out.join('\n');
751
770
  }
752
771
  export async function phaseCompose(deps, refined, research, qa) {
753
- const contracts = await phaseContractsBlock(deps);
772
+ const contracts = await phaseCarriedBlocks(deps);
754
773
  return runWithEmphasisRetry(deps, 'compose', 'read', problem => COMPOSE_PROMPT(refined, research, qa, problem, contracts), text => {
755
774
  // Trim any "here's the spec:" preamble before validating, so a
756
775
  // strippable lead-in doesn't burn a full retry — and the stored
@@ -772,7 +791,7 @@ export async function phaseCompose(deps, refined, research, qa) {
772
791
  return { ok: true, value: stripped };
773
792
  }, problem => new Error(`compose_invalid: ${problem}`));
774
793
  }
775
- export async function phaseCritique(deps, spec, refined, qa) {
794
+ export async function phaseCritique(deps, spec, refined, qa, planContext) {
776
795
  // Fast triage before the expensive full rewrite. The rewrite regenerates
777
796
  // the entire spec from scratch and is the costliest tail of the pipeline
778
797
  // (observed up to ~240s). Most compose drafts are already good, so we first
@@ -813,6 +832,24 @@ export async function phaseCritique(deps, spec, refined, qa) {
813
832
  if (wiringProbe) {
814
833
  deps.logDebug?.(`synthesized wiring flagged in spec: ${wiring.map(w => w.line).join(' | ')}`);
815
834
  }
835
+ // DETERMINISTIC plan-contradiction probe (mx5 run 11, goal D): a VERIFY line
836
+ // asserting the ABSENCE of an artifact the plan pins elsewhere — a path a prior
837
+ // task already shipped to disk, a sibling title's deliverable, a contract-pinned
838
+ // boundary. Run 11: the scope fence leaked into TASK_0009's verify as "the admin
839
+ // page must NOT exist" (TASK_0008's deliverable); the guaranteed FAIL became an
840
+ // accepted debt that the final-gate autofix then "fixed" by deleting the sibling's
841
+ // work. The conflict must die here, at spec time — forced into the rewrite like
842
+ // the skip-escape finding; delete-tasks keep their check by declaring the delete.
843
+ const absenceConflicts = findAbsenceConflicts(spec, {
844
+ fileExists: p => existsSync(resolve(deps.cwd, p)),
845
+ siblingTitles: siblingTitlesFromPlanContext(planContext),
846
+ contracts: registryRaw
847
+ });
848
+ const absenceProbe = absenceConflicts.length > 0 ? absenceProbeText(absenceConflicts) : null;
849
+ if (absenceProbe) {
850
+ deps.logDebug?.('plan-contradiction flagged in VERIFY: '
851
+ + absenceConflicts.map(c => `${c.assertion.target} (${c.against})`).join(' | '));
852
+ }
816
853
  let triageDefects = null;
817
854
  if (parseVerifyBlock(spec) !== null) {
818
855
  const tTriage = Date.now();
@@ -829,21 +866,23 @@ export async function phaseCritique(deps, spec, refined, qa) {
829
866
  }
830
867
  deps.recordSubStep?.('triage', Date.now() - tTriage);
831
868
  if (verdict !== null) {
832
- // A deterministic skip-escape OR synthesized-wiring finding overrides a CLEAN
833
- // triage: the draft must be rewritten to resolve it even if the model judged
834
- // the rest clean (the model does not self-discover either reliably).
869
+ // A deterministic skip-escape, synthesized-wiring, or plan-contradiction
870
+ // finding overrides a CLEAN triage: the draft must be rewritten to resolve
871
+ // it even if the model judged the rest clean (the model does not
872
+ // self-discover any of them reliably).
835
873
  if (isCritiqueClean(verdict)) {
836
- if (skipDefects === null && wiringProbe === null)
874
+ if (skipDefects === null && wiringProbe === null && absenceProbe === null) {
837
875
  return spec;
876
+ }
838
877
  }
839
878
  else {
840
879
  triageDefects = verdict.trim();
841
880
  }
842
881
  }
843
882
  }
844
- // Merge the deterministic skip-escape + synthesized-wiring defects with any triage
845
- // defects for the rewrite (both are forced FOCUS items).
846
- const rewriteDefects = [skipDefects, wiringProbe, triageDefects].filter(Boolean).join('\n\n') || null;
883
+ // Merge the deterministic skip-escape + synthesized-wiring + plan-contradiction
884
+ // defects with any triage defects for the rewrite (all are forced FOCUS items).
885
+ const rewriteDefects = [skipDefects, wiringProbe, absenceProbe, triageDefects].filter(Boolean).join('\n\n') || null;
847
886
  const tRewrite = Date.now();
848
887
  try {
849
888
  return await runWithEmphasisRetry(deps, 'critique', 'read', problem => CRITIQUE_PROMPT(spec, refined, qa, problem !== null, rewriteDefects, contractsBlock), text => {
@@ -863,7 +902,7 @@ export async function phaseCritique(deps, spec, refined, qa) {
863
902
  // ─── Critique with fallback ──────────────────────────────────────────────────
864
903
  export async function critiqueWithFallback(d, p) {
865
904
  try {
866
- return await phaseCritique(d, p.spec, p.refined, p.qa);
905
+ return await phaseCritique(d, p.spec, p.refined, p.qa, p.planContext);
867
906
  }
868
907
  catch (err) {
869
908
  const msg = err instanceof Error ? err.message : String(err);
@@ -324,6 +324,8 @@ VERIFY must exercise the surface area the task actually touches. Draw VERIFY com
324
324
  - Python / Go / Rust / other source changes → MUST include the language's standard verification from TOOLING (e.g. \`pytest\`, \`go test ./...\`, \`cargo test\`) plus lint/typecheck if configured.
325
325
  - Config / infra-only changes with no executable verification → state that explicitly with a single command that re-reads or validates the config (e.g. \`docker compose config\`, \`nginx -t\`, \`yamllint file.yml\`). Never leave VERIFY with only \`true\` or \`echo ok\`.
326
326
 
327
+ When this task is one step of a larger plan: sibling steps' deliverables may already exist in the tree and more will land after this task. NEVER write a VERIFY check that fails because sibling work exists (e.g. "file X must not exist" when another step owns X). The plan context forbids you from BUILDING other steps' work — it does not make their work absent. Verify what THIS task adds or changes.
328
+
327
329
  If TOOLING is empty for a category the change clearly touches, still include the best-effort standard command for that ecosystem (e.g. \`npx tsc --noEmit\` for a TS repo with no script) and note that the receiving agent may need to install it.
328
330
 
329
331
  If the research contains an "API CORRECTIONS" section, it is AUTHORITATIVE — each line was verified against the installed types. For every correction: use the import it prescribes, never the specifier it marks non-existent, and add a CONSTRAINT recording it verbatim (e.g. Use \`import { sql } from "bun"\`; \`bun:sql\` is not a module — do not import it or declare a module for it). This overrides any conflicting identifier carried up from the refined task or the spec doc.
@@ -0,0 +1,32 @@
1
+ export type RenderOutcome = {
2
+ outcome: 'pass';
3
+ detail: string;
4
+ } | {
5
+ outcome: 'fail';
6
+ detail: string;
7
+ } | {
8
+ outcome: 'skip';
9
+ note: string;
10
+ };
11
+ /**
12
+ * A launchable Chrome-family binary, or null when this box has none: the explicit
13
+ * CHROME_BIN override first, then the Playwright cache (headless shell — reliable
14
+ * on network URLs), then a system browser on PATH as a last resort. Null → the
15
+ * render check SKIPs (env gap) — it never installs anything.
16
+ */
17
+ export declare function findHeadlessBrowser(): string | null;
18
+ /**
19
+ * Judge a RENDERED (post-JS) DOM: the body must carry visible text or concrete
20
+ * visual/interactive elements. Pure text analysis so the judgment is unit-tested
21
+ * against real captured DOMs; `detail` describes what was (or wasn't) found.
22
+ */
23
+ export declare function judgeRenderedDom(html: string): {
24
+ ok: boolean;
25
+ detail: string;
26
+ };
27
+ /**
28
+ * Load `url` once in a headless Chrome and judge the rendered DOM. Blocking
29
+ * (spawnSync) by design — the caller holds the booted server alive exactly for
30
+ * this window. `browser` is injectable for tests; the default is discovery.
31
+ */
32
+ export declare function runRenderCheck(url: string, browser?: string | null): RenderOutcome;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * render-check — one headless-browser page load against the booted app's live
3
+ * listener, judging whether the client actually RENDERED anything (mx5 runs 8 and
4
+ * 11).
5
+ *
6
+ * The failure class: every "renders without runtime errors" check in the pipeline
7
+ * was curl — and curl cannot execute JavaScript. Run 8 shipped an app whose ESM
8
+ * bundle was loaded in a classic script tag: HTTP 200 on every route, permanently
9
+ * BLANK page, all gates green. Run 11 shipped a router with no <Switch> (the 404
10
+ * fallback rendered on every page) — invisible to every gate for the same reason.
11
+ * The gate's boot check proves a LISTENER exists (run 10); this proves the
12
+ * listener serves a page whose client code MOUNTS something.
13
+ *
14
+ * Mechanism, deterministic and dependency-free: discover a Chrome-family binary
15
+ * (system chromium/chrome or the Playwright browser cache — nothing is installed,
16
+ * only found), load the page once with `--headless --dump-dom` (which executes the
17
+ * page's JS under a virtual-time budget), and judge the RENDERED body: it must
18
+ * contain visible text or concrete visual/interactive elements. A blank mount
19
+ * point after JS ran is the run-8 class — FAIL with the body's shape. What it
20
+ * deliberately does NOT judge: correctness of what rendered (run 11's 404-below-
21
+ * login needs app knowledge no generic gate has).
22
+ *
23
+ * Env-gap contract as everywhere: no browser found, a browser that cannot launch,
24
+ * or a dump that produced nothing → SKIP with a note (the caller surfaces it as
25
+ * UNOBSERVED), never a false FAIL. Only a page the browser really loaded and
26
+ * rendered EMPTY fails.
27
+ */
28
+ import { spawnSync } from 'node:child_process';
29
+ import { existsSync, readdirSync } from 'node:fs';
30
+ import * as os from 'node:os';
31
+ import * as path from 'node:path';
32
+ /** PATH names tried in order for a system Chrome-family binary. */
33
+ const CHROME_PATH_CANDIDATES = [
34
+ 'chromium',
35
+ 'chromium-browser',
36
+ 'google-chrome-stable',
37
+ 'google-chrome',
38
+ 'chrome'
39
+ ];
40
+ /** Does `bin` resolve on PATH? (`command -v` shape, portable via spawnSync.) */
41
+ function onPath(bin) {
42
+ const probe = process.platform === 'win32' ? 'where' : 'which';
43
+ const r = spawnSync(probe, [bin], { encoding: 'utf8', timeout: 4000 });
44
+ return !r.error && r.status === 0 && (r.stdout ?? '').trim().length > 0;
45
+ }
46
+ /**
47
+ * The newest Playwright-cache Chromium, if any — the HEADLESS SHELL preferred over
48
+ * the full build. Found, never installed: the cache exists on any box that ever ran
49
+ * Playwright browsers (the mx5-class projects install it as their own test
50
+ * dependency). The headless shell is purpose-built for `--dump-dom` and returns
51
+ * promptly on a network URL where the full system chromium can stall under a
52
+ * virtual-time budget (validated live on this box: shell exits 0 in ~1s, full
53
+ * chromium times out at 30s on the same http:// URL).
54
+ */
55
+ function playwrightCachedChromium() {
56
+ const cache = process.env.PLAYWRIGHT_BROWSERS_PATH
57
+ ?? (process.platform === 'darwin' ?
58
+ path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright')
59
+ : path.join(os.homedir(), '.cache', 'ms-playwright'));
60
+ let entries;
61
+ try {
62
+ entries = readdirSync(cache);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ // Headless shell first (fast, purpose-built), then the full chromium build.
68
+ // Within each family the newest revision wins (revs sort ascending → reverse).
69
+ const families = [
70
+ {
71
+ prefix: 'chromium_headless_shell-',
72
+ rels: [
73
+ path.join('chrome-headless-shell-linux64', 'chrome-headless-shell'),
74
+ path.join('chrome-linux', 'headless_shell')
75
+ ]
76
+ },
77
+ {
78
+ prefix: 'chromium-',
79
+ rels: [path.join('chrome-linux64', 'chrome'), path.join('chrome-linux', 'chrome')]
80
+ }
81
+ ];
82
+ for (const { prefix, rels } of families) {
83
+ const revs = entries.filter(e => e.startsWith(prefix)).sort();
84
+ for (const rev of revs.reverse()) {
85
+ for (const rel of rels) {
86
+ const p = path.join(cache, rev, rel);
87
+ if (existsSync(p))
88
+ return p;
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * A launchable Chrome-family binary, or null when this box has none: the explicit
96
+ * CHROME_BIN override first, then the Playwright cache (headless shell — reliable
97
+ * on network URLs), then a system browser on PATH as a last resort. Null → the
98
+ * render check SKIPs (env gap) — it never installs anything.
99
+ */
100
+ export function findHeadlessBrowser() {
101
+ const explicit = process.env.CHROME_BIN;
102
+ if (explicit && existsSync(explicit))
103
+ return explicit;
104
+ const cached = playwrightCachedChromium();
105
+ if (cached)
106
+ return cached;
107
+ for (const bin of CHROME_PATH_CANDIDATES) {
108
+ if (onPath(bin))
109
+ return bin;
110
+ }
111
+ return null;
112
+ }
113
+ /** Elements whose presence means the page rendered CONCRETE UI even with no text. */
114
+ const VISUAL_ELEMENT_RE = /<(?:img|svg|canvas|video|audio|input|button|textarea|select|iframe)\b/i;
115
+ /**
116
+ * Judge a RENDERED (post-JS) DOM: the body must carry visible text or concrete
117
+ * visual/interactive elements. Pure text analysis so the judgment is unit-tested
118
+ * against real captured DOMs; `detail` describes what was (or wasn't) found.
119
+ */
120
+ export function judgeRenderedDom(html) {
121
+ const bodyMatch = /<body\b[^>]*>([\s\S]*)<\/body>/i.exec(html);
122
+ // No <body> at all in a dumped DOM → the browser rendered something degenerate;
123
+ // judge the whole document rather than fail on shape.
124
+ const body = bodyMatch ? bodyMatch[1] : html;
125
+ const visible = body
126
+ .replace(/<(script|style|template|noscript)\b[\s\S]*?<\/\1>/gi, '')
127
+ .replace(/<!--[\s\S]*?-->/g, '');
128
+ const text = visible
129
+ .replace(/<[^>]*>/g, ' ')
130
+ .replace(/\s+/g, ' ')
131
+ .trim();
132
+ if (text.length > 0) {
133
+ return { ok: true, detail: `rendered visible text ("${text.slice(0, 80)}")` };
134
+ }
135
+ if (VISUAL_ELEMENT_RE.test(visible)) {
136
+ return { ok: true, detail: 'rendered visual/interactive elements (no text)' };
137
+ }
138
+ return {
139
+ ok: false,
140
+ detail: 'the rendered body is EMPTY after client JS executed — no visible text, no '
141
+ + 'visual or interactive elements (the blank-page class: HTTP serves, nothing mounts)'
142
+ };
143
+ }
144
+ /** Wall-clock cap for the whole browser run; virtual-time budget for the page JS. */
145
+ const RENDER_TIMEOUT_MS = 30_000;
146
+ const VIRTUAL_TIME_BUDGET_MS = 8_000;
147
+ /**
148
+ * Load `url` once in a headless Chrome and judge the rendered DOM. Blocking
149
+ * (spawnSync) by design — the caller holds the booted server alive exactly for
150
+ * this window. `browser` is injectable for tests; the default is discovery.
151
+ */
152
+ export function runRenderCheck(url, browser) {
153
+ const bin = browser === undefined ? findHeadlessBrowser() : browser;
154
+ if (!bin) {
155
+ return {
156
+ outcome: 'skip',
157
+ note: 'no headless Chrome-family browser found on this box (PATH, CHROME_BIN, Playwright cache)'
158
+ };
159
+ }
160
+ const r = spawnSync(bin, [
161
+ '--headless',
162
+ '--disable-gpu',
163
+ '--no-sandbox',
164
+ '--disable-dev-shm-usage',
165
+ `--virtual-time-budget=${VIRTUAL_TIME_BUDGET_MS}`,
166
+ '--dump-dom',
167
+ url
168
+ ], { encoding: 'utf8', timeout: RENDER_TIMEOUT_MS, env: { ...process.env } });
169
+ if (r.error || r.status === null) {
170
+ return { outcome: 'skip', note: `browser did not run (${r.error?.message ?? 'timeout'})` };
171
+ }
172
+ const dom = (r.stdout ?? '').trim();
173
+ if (r.status !== 0 || dom.length === 0) {
174
+ // A crash/empty dump is a browser/env condition on this box, not proof the
175
+ // app is blank — skip with the tail so the trail explains it.
176
+ const tail = (r.stderr ?? '').trim().slice(-200);
177
+ return {
178
+ outcome: 'skip',
179
+ note: `browser exited ${r.status} with no DOM${tail ? ` — ${tail}` : ''}`
180
+ };
181
+ }
182
+ const judged = judgeRenderedDom(dom);
183
+ return judged.ok ?
184
+ { outcome: 'pass', detail: judged.detail }
185
+ : { outcome: 'fail', detail: judged.detail };
186
+ }
@@ -0,0 +1,88 @@
1
+ export interface RequirementEntry {
2
+ /** The verbatim quote from the source doc — the obligation. */
3
+ quote: string;
4
+ /** Where it came from (heading/section, or 'prose'). */
5
+ anchor: string;
6
+ }
7
+ export declare function requirementsFile(cwd: string): string;
8
+ /** Parse `REQUIREMENT: "<quote>" [anchor: …]` lines (mirrors parseContractLines). */
9
+ export declare function parseRequirementLines(text: string): RequirementEntry[];
10
+ /** THE ANTI-SYNTHESIS GUARD: keep only entries whose quote is a normalised
11
+ * substring of the source doc (same rule as keepGroundedContracts). Does NOT
12
+ * cap — capping is capRequirements' job, which protects obligation-marked
13
+ * passages from doc-order truncation. */
14
+ export declare function keepGroundedRequirements(entries: RequirementEntry[], sourceDoc: string): RequirementEntry[];
15
+ /**
16
+ * Bound the list WITHOUT doc-order truncation: measured live, an eager model
17
+ * extracts 40+ items top-down (every §1 decision row), so a plain first-N cap
18
+ * systematically drops the TAIL sections — exactly where mx5 keeps its testing
19
+ * obligations. Rule (deterministic priority, not a knob): entries quoting an
20
+ * obligation-marked passage survive first; the remainder fills in given order.
21
+ */
22
+ export declare function capRequirements(entries: RequirementEntry[], passages: string[]): RequirementEntry[];
23
+ /**
24
+ * DETERMINISTIC RECALL FLOOR (same medicine as the launch-contract checklist):
25
+ * paragraphs carrying an obligation marker (word-bounded "required"/"must").
26
+ * Extraction recall over a 20KB doc is the weak model's, and it is variance-
27
+ * prone — measured live, 1 of 5 runs kept 16 quotes with ZERO §10 items. The
28
+ * host enumerates the marked passages; the prompt lists their head lines as a
29
+ * checklist, and uncoveredPassages() below turns "a marked passage produced no
30
+ * quote" into hard evidence for one forced re-extraction.
31
+ */
32
+ export declare function enumerateObligationPassages(doc: string): string[];
33
+ /** Marked passages none of the kept quotes came from — the hard evidence that
34
+ * extraction recall failed there (a kept quote "covers" a passage when the
35
+ * passage contains it, normalised). */
36
+ export declare function uncoveredPassages(passages: string[], kept: RequirementEntry[]): string[];
37
+ /** Reprompt hint for the forced re-extraction over uncovered passages. */
38
+ export declare function extractionRetryHint(uncovered: string[]): string;
39
+ /** The plan-time extraction prompt. Runs with --no-tools; every quote is
40
+ * re-grounded host-side, so guessing wastes effort. Spec-shape-agnostic.
41
+ * `passages` is enumerateObligationPassages' checklist ([] ⇒ prompt unchanged). */
42
+ export declare const REQUIREMENT_EXTRACT_PROMPT: (feature: string, passages?: string[]) => string;
43
+ export type ReqMapping = {
44
+ kind: 'task';
45
+ task: number;
46
+ } | {
47
+ kind: 'cross';
48
+ } | {
49
+ kind: 'none';
50
+ };
51
+ /** Per-requirement coverage verdicts against a task list. Runs with --no-tools. */
52
+ export declare const COVERAGE_MAP_PROMPT: (requirements: RequirementEntry[], titles: string[]) => string;
53
+ /**
54
+ * Parse the mapping output. Index-aligned with `requirements` (0-based); a
55
+ * requirement the model skipped, or mapped to an out-of-range task, is `none` —
56
+ * distrust by default: an unaccounted requirement is exactly what this gate
57
+ * exists to surface, so parsing leniency must never manufacture coverage.
58
+ */
59
+ export declare function parseCoverageMap(text: string, reqCount: number, taskCount: number): ReqMapping[];
60
+ export interface CoverageAccounting {
61
+ mapped: Array<{
62
+ req: RequirementEntry;
63
+ task: number;
64
+ }>;
65
+ crossCutting: RequirementEntry[];
66
+ /** Requirements NO task covers — drives the decompose reprompt / surfacing. */
67
+ unmapped: RequirementEntry[];
68
+ }
69
+ /** Deterministic accounting over the parsed map — the host, not the model,
70
+ * decides completeness. */
71
+ export declare function accountCoverage(requirements: RequirementEntry[], mappings: ReqMapping[]): CoverageAccounting;
72
+ /** The stored carried-requirements text ('' when none recorded). */
73
+ export declare function readRequirements(cwd: string): Promise<string>;
74
+ /**
75
+ * Append carried requirements (cross-cutting, plus any left unmapped after the
76
+ * retry rounds — better carried into every task than silently lost), deduped
77
+ * against what is stored. Host-side only; children never write it. Best-effort.
78
+ */
79
+ export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[]): Promise<void>;
80
+ /**
81
+ * The read-only block refine/compose receive when carried requirements exist.
82
+ * Verbatim content travels with every task (the directive pattern that works),
83
+ * and the VERIFY mandate is explicit — goal C rides here.
84
+ */
85
+ export declare function buildRequirementsBlock(requirements: string): string;
86
+ /** The decompose-prompt ledger block (goal E's belt): the grounded requirement
87
+ * list rides into decompose so structure-mirroring can't discharge it. */
88
+ export declare function buildRequirementsLedger(requirements: RequirementEntry[]): string;