@mjasnikovs/pi-task 0.18.14 → 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.
- package/README.md +2 -2
- package/dist/task/accept-debt.d.ts +28 -1
- package/dist/task/accept-debt.js +61 -3
- package/dist/task/auto-io.d.ts +4 -2
- package/dist/task/auto-io.js +6 -3
- package/dist/task/auto-orchestrator.d.ts +1 -0
- package/dist/task/auto-orchestrator.js +135 -19
- package/dist/task/auto-prompts.d.ts +5 -5
- package/dist/task/auto-prompts.js +9 -2
- package/dist/task/contracts.d.ts +8 -0
- package/dist/task/contracts.js +4 -2
- package/dist/task/decompose-fidelity.d.ts +47 -0
- package/dist/task/decompose-fidelity.js +132 -0
- package/dist/task/final-gate-fix.d.ts +22 -3
- package/dist/task/final-gate-fix.js +72 -7
- package/dist/task/final-gate.d.ts +48 -1
- package/dist/task/final-gate.js +182 -34
- package/dist/task/gate-deps.d.ts +7 -0
- package/dist/task/gate-deps.js +37 -1
- package/dist/task/launch-contract.d.ts +36 -1
- package/dist/task/launch-contract.js +80 -2
- package/dist/task/phases.d.ts +13 -1
- package/dist/task/phases.js +50 -11
- package/dist/task/prompts.js +2 -0
- package/dist/task/render-check.d.ts +32 -0
- package/dist/task/render-check.js +186 -0
- package/dist/task/requirements.d.ts +88 -0
- package/dist/task/requirements.js +331 -0
- package/dist/task/verify-reconcile.d.ts +36 -0
- package/dist/task/verify-reconcile.js +203 -0
- package/dist/task/write-guard.d.ts +52 -0
- package/dist/task/write-guard.js +112 -0
- package/package.json +1 -1
- package/dist/task/_ab.d.ts +0 -1
- package/dist/task/_ab.js +0 -68
- package/dist/task/task-file.d.ts +0 -14
- package/dist/task/task-file.js +0 -15
- package/dist/think-test/cli.d.ts +0 -1
- package/dist/think-test/cli.js +0 -98
- package/dist/think-test/client.d.ts +0 -26
- package/dist/think-test/client.js +0 -37
- package/dist/think-test/compressor.d.ts +0 -5
- package/dist/think-test/compressor.js +0 -25
- package/dist/think-test/judge.d.ts +0 -4
- package/dist/think-test/judge.js +0 -11
- package/dist/think-test/score.d.ts +0 -8
- package/dist/think-test/score.js +0 -22
- package/dist/think-test/serialize.d.ts +0 -19
- package/dist/think-test/serialize.js +0 -41
- package/dist/think-test/transcript.d.ts +0 -7
- package/dist/think-test/transcript.js +0 -41
- package/dist/think-test/transform.d.ts +0 -6
- package/dist/think-test/transform.js +0 -24
- package/dist/think-test/types.d.ts +0 -45
- package/dist/think-test/types.js +0 -1
package/dist/task/phases.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
833
|
-
// triage: the draft must be rewritten to resolve
|
|
834
|
-
// the rest clean (the model does not
|
|
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
|
|
845
|
-
// defects for the rewrite (
|
|
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);
|
package/dist/task/prompts.js
CHANGED
|
@@ -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;
|