@mjasnikovs/pi-task 0.37.4 → 0.37.5
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/dist/task/final-gate.js
CHANGED
|
@@ -52,6 +52,7 @@ import * as path from 'node:path';
|
|
|
52
52
|
import { runRepoHealthCheck, discoverHealthCommands } from './repo-health-check.js';
|
|
53
53
|
import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtNote, annotateDebtConflicts } from './accept-debt.js';
|
|
54
54
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
55
|
+
import { readLaunchManifest, inertLaunchContractNote } from './launch-manifest.js';
|
|
55
56
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
56
57
|
import { runRenderCheck } from './render-check.js';
|
|
57
58
|
import { collectProjectEnv, pinnedLocalPort, runDeepRenderCheck } from './deep-render-check.js';
|
|
@@ -1378,11 +1379,28 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1378
1379
|
// scripts that fell through decompose and shipped missing, unchecked. Diff the
|
|
1379
1380
|
// plan-time-extracted declared scripts against the manifest; a missing one is a
|
|
1380
1381
|
// launch-surface defect. FP-safe: empty declared list (nothing grounded) → no check.
|
|
1382
|
+
//
|
|
1383
|
+
// THE DIFF IS INERT WITHOUT A MANIFEST (nexttask 16A). It used to diff against
|
|
1384
|
+
// `Object.keys(packageScripts(cwd))`, whose catch returns {} — so a project with
|
|
1385
|
+
// NO package.json was indistinguishable from one with no scripts, and every
|
|
1386
|
+
// declared script was reported missing in wording naming a file the project was
|
|
1387
|
+
// never meant to have. Nothing upstream is npm-shaped (the extractor scrapes any
|
|
1388
|
+
// design that says "script"), and this text seeds the autofix child's prompt, so
|
|
1389
|
+
// on a CMake/cargo project the likely repair was to write a package.json.
|
|
1390
|
+
// readLaunchManifest resolves package.json, else a Makefile's targets, else
|
|
1391
|
+
// nothing — and nothing means no failure plus a note, never a silent pass.
|
|
1381
1392
|
const declared = await readDeclaredScripts(cwd);
|
|
1393
|
+
const contractNotes = [];
|
|
1382
1394
|
if (declared.length > 0) {
|
|
1383
|
-
const
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1395
|
+
const manifest = readLaunchManifest(cwd);
|
|
1396
|
+
if (manifest.kind === 'none') {
|
|
1397
|
+
contractNotes.push(inertLaunchContractNote(declared, manifest));
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
const missing = missingDeclaredScripts(declared, manifest.names);
|
|
1401
|
+
if (missing.length > 0) {
|
|
1402
|
+
fail(`launch contract: the design declares script(s) the shipped ${manifest.file} does not expose: ${missing.join(', ')} (declared: ${declared.join(', ')})`);
|
|
1403
|
+
}
|
|
1386
1404
|
}
|
|
1387
1405
|
}
|
|
1388
1406
|
// Serve-entry closure (mx5 run 18, nexttask 2B): the tree builds a server app,
|
|
@@ -1416,7 +1434,12 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1416
1434
|
// harvest lever refuted at discoverIntegrationCommands it cannot inject a fabricated
|
|
1417
1435
|
// failure.
|
|
1418
1436
|
if (lockCmds.length === 0 && cmds.length === 0 && !boot && failures.length === 0) {
|
|
1419
|
-
|
|
1437
|
+
// The inert-contract note rides here too: a non-npm project carrying a launch
|
|
1438
|
+
// contract usually discovers no command either, and that is exactly the run
|
|
1439
|
+
// whose silence must not read as "the contract was checked and was fine".
|
|
1440
|
+
const note = [unobservedVerdict({ discovered: 0, observed: 0 }) ?? '', ...contractNotes]
|
|
1441
|
+
.filter(n => n !== '')
|
|
1442
|
+
.join(' ');
|
|
1420
1443
|
return withDebts({ ok: true, unobserved: note, reason: note });
|
|
1421
1444
|
}
|
|
1422
1445
|
const ran = [];
|
|
@@ -1471,6 +1494,11 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1471
1494
|
// A declared script the manifest doesn't expose is already a launch-contract
|
|
1472
1495
|
// failure above; executing it too would double-report (pre-aggregation the
|
|
1473
1496
|
// contract diff early-returned, so this loop could assume presence).
|
|
1497
|
+
// EXECUTION STAYS npm-ONLY. The diff above now also speaks Makefile (16A),
|
|
1498
|
+
// but the runner below is literally `bun run <name>`; on a Makefile project
|
|
1499
|
+
// `present` is empty, so every declared target is skipped rather than run
|
|
1500
|
+
// through the wrong tool. Widening the RUNNER is a separate lever with its
|
|
1501
|
+
// own A/B, not a free rider on an inertness fix.
|
|
1474
1502
|
const present = new Set(Object.keys(packageScripts(cwd)).map(s => s.toLowerCase()));
|
|
1475
1503
|
const scripts = packageScripts(cwd);
|
|
1476
1504
|
// CONFIG-GAP INPUTS (mx5 run 20), read once: the tracked file list and the
|
|
@@ -1704,7 +1732,8 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
1704
1732
|
const unobserved = [
|
|
1705
1733
|
bootUnobserved,
|
|
1706
1734
|
unobservedVerdict({ discovered: dynAttempted, observed: dynObserved }),
|
|
1707
|
-
...configGapNotes
|
|
1735
|
+
...configGapNotes,
|
|
1736
|
+
...contractNotes
|
|
1708
1737
|
]
|
|
1709
1738
|
.filter(n => n !== null)
|
|
1710
1739
|
.join(' ');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Which manifest kind the diff resolved for a tree. `none` ⇒ the diff is INERT. */
|
|
2
|
+
export type LaunchManifestKind = 'npm' | 'make' | 'none';
|
|
3
|
+
export interface LaunchManifest {
|
|
4
|
+
kind: LaunchManifestKind;
|
|
5
|
+
/** The manifest the diff is taken against, for the failure text. '' when none. */
|
|
6
|
+
file: string;
|
|
7
|
+
/** Entrypoint names the manifest exposes: npm script keys / Makefile targets. */
|
|
8
|
+
names: string[];
|
|
9
|
+
/** Why nothing is checkable (kind === 'none' only). */
|
|
10
|
+
why?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The explicit targets a Makefile declares. Deliberately conservative — this list
|
|
14
|
+
* only ever has to answer "does the project expose `migrate`", so a missed exotic
|
|
15
|
+
* target costs a false FAIL and is the one error worth avoiding:
|
|
16
|
+
* • recipe lines (leading TAB) are skipped — they are shell, not targets;
|
|
17
|
+
* • `:=` / `::=` / `?=` / `+=` assignments are skipped;
|
|
18
|
+
* • pattern rules (`%.o: %.c`) and dot-targets (`.PHONY:`) fail TARGET_NAME_RE,
|
|
19
|
+
* which is also why the names listed AFTER `.PHONY:` are ignored here: they are
|
|
20
|
+
* prerequisites, and every one of them that is real is declared by its own rule.
|
|
21
|
+
* Multiple targets on one line (`build test:`) all count.
|
|
22
|
+
*/
|
|
23
|
+
export declare function makeTargets(src: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the manifest the launch contract may be diffed against.
|
|
26
|
+
*
|
|
27
|
+
* A package.json that exists but does not parse resolves to `none`, not to "zero
|
|
28
|
+
* scripts": diffing against a manifest you could not read is exactly the mistake
|
|
29
|
+
* this module exists to stop, and a broken manifest is the static checks' business.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readLaunchManifest(cwd: string): LaunchManifest;
|
|
32
|
+
/**
|
|
33
|
+
* The one line an INERT contract leaves behind. It rides in the gate's UNOBSERVED
|
|
34
|
+
* channel rather than in `warnings` for the reason nexttask 16A gives: a check that
|
|
35
|
+
* silently did nothing must not read later as a check that passed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function inertLaunchContractNote(declared: string[], manifest: LaunchManifest): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launch-manifest — WHICH manifest the launch-contract diff is entitled to diff
|
|
3
|
+
* against, and whether there is one at all (nexttask 16A).
|
|
4
|
+
*
|
|
5
|
+
* The defect this closes (unobserved, proven by construction). The extraction end
|
|
6
|
+
* of the launch contract has no ecosystem test anywhere in it:
|
|
7
|
+
* `enumerateScriptCandidates` scrapes backticked, script-name-shaped tokens out of
|
|
8
|
+
* any design paragraph that says "script", and `keepGroundedScripts` can only DROP
|
|
9
|
+
* a candidate. So a CMake / cargo / poetry / Makefile design saying *"the Makefile
|
|
10
|
+
* must expose `build`, `test`, `migrate`"* records exactly the same artifact an npm
|
|
11
|
+
* design does. The diff end hardcoded the ecosystem anyway:
|
|
12
|
+
*
|
|
13
|
+
* missingDeclaredScripts(declared, Object.keys(packageScripts(cwd)))
|
|
14
|
+
* …where packageScripts' catch returns {} — so "this project has no
|
|
15
|
+
* package.json" was indistinguishable from "its package.json declares no
|
|
16
|
+
* scripts", and every declared script was reported missing, at rank 1, in
|
|
17
|
+
* wording that NAMES a file the project was never meant to have.
|
|
18
|
+
*
|
|
19
|
+
* That text seeds the autofix child's prompt (final-gate.ts's FinalGateOutcome.reason),
|
|
20
|
+
* so the most likely repair on a CMake project was to write a package.json.
|
|
21
|
+
*
|
|
22
|
+
* IAR1 is how close this got: `plan-debug.log:10` records "launch-contract
|
|
23
|
+
* extraction: 0 grounded script(s) kept from 3 emitted" — a non-npm project that
|
|
24
|
+
* reached extraction and emitted three candidates, saved only by its design not
|
|
25
|
+
* backticking them in a "script" paragraph.
|
|
26
|
+
*
|
|
27
|
+
* THE RULE, and it is deliberately two ecosystems wide, not three:
|
|
28
|
+
* • package.json present and parseable ⇒ diff against its `scripts` keys, exactly
|
|
29
|
+
* as before (byte-identical failure text on every npm tree — mx5's missing
|
|
30
|
+
* `build`/`seed` is a TRUE positive and must keep failing).
|
|
31
|
+
* • no package.json but a Makefile ⇒ diff against its TARGETS. The capability was
|
|
32
|
+
* already in final-gate.ts (`makeHasTarget`); the diff simply never called it.
|
|
33
|
+
* • neither ⇒ the check is INERT. No failure, and a note saying the contract was
|
|
34
|
+
* recorded but is not checkable here, so the silence is not read later as a pass
|
|
35
|
+
* (the repo-health-check / env-template-closure discipline: ENOENT = pass, and a
|
|
36
|
+
* check must never invent a file the project chose not to have).
|
|
37
|
+
*
|
|
38
|
+
* cargo/poetry/gradle are NOT here on purpose. The corpus on this box contains one
|
|
39
|
+
* project that ever recorded a launch contract (mx5, npm) and one non-npm project
|
|
40
|
+
* that reached extraction (IAR1, CMake). A third ecosystem would be a guess.
|
|
41
|
+
*/
|
|
42
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
43
|
+
import * as path from 'node:path';
|
|
44
|
+
/** GNU make's own lookup order, and nothing beyond it — this is one ecosystem. */
|
|
45
|
+
const MAKEFILE_NAMES = ['GNUmakefile', 'makefile', 'Makefile'];
|
|
46
|
+
/** A target we are willing to name in a failure: an ordinary word-shaped target. */
|
|
47
|
+
const TARGET_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,39}$/i;
|
|
48
|
+
/**
|
|
49
|
+
* A variable assignment, in every form make accepts (`=`, `:=`, `::=`, `?=`, `+=`,
|
|
50
|
+
* `!=`, with an optional `override`/`export`). Matched BEFORE the target rule
|
|
51
|
+
* because `FLAGS ::= -O2` otherwise reads as a target named `FLAGS`.
|
|
52
|
+
*/
|
|
53
|
+
const ASSIGN_RE = /^(?:override\s+|export\s+)?[A-Za-z_][A-Za-z0-9_.]*\s*(?:::|:|\?|\+|!)?=/;
|
|
54
|
+
/**
|
|
55
|
+
* The explicit targets a Makefile declares. Deliberately conservative — this list
|
|
56
|
+
* only ever has to answer "does the project expose `migrate`", so a missed exotic
|
|
57
|
+
* target costs a false FAIL and is the one error worth avoiding:
|
|
58
|
+
* • recipe lines (leading TAB) are skipped — they are shell, not targets;
|
|
59
|
+
* • `:=` / `::=` / `?=` / `+=` assignments are skipped;
|
|
60
|
+
* • pattern rules (`%.o: %.c`) and dot-targets (`.PHONY:`) fail TARGET_NAME_RE,
|
|
61
|
+
* which is also why the names listed AFTER `.PHONY:` are ignored here: they are
|
|
62
|
+
* prerequisites, and every one of them that is real is declared by its own rule.
|
|
63
|
+
* Multiple targets on one line (`build test:`) all count.
|
|
64
|
+
*/
|
|
65
|
+
export function makeTargets(src) {
|
|
66
|
+
const out = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const raw of src.replace(/\r\n?/g, '\n').split('\n')) {
|
|
69
|
+
if (raw.startsWith('\t') || raw.trim().length === 0 || raw.trimStart().startsWith('#'))
|
|
70
|
+
continue;
|
|
71
|
+
if (ASSIGN_RE.test(raw))
|
|
72
|
+
continue;
|
|
73
|
+
const m = /^([^:#=]+):(?![=])/.exec(raw);
|
|
74
|
+
if (!m)
|
|
75
|
+
continue;
|
|
76
|
+
for (const tok of m[1].trim().split(/\s+/)) {
|
|
77
|
+
if (!TARGET_NAME_RE.test(tok))
|
|
78
|
+
continue;
|
|
79
|
+
const key = tok.toLowerCase();
|
|
80
|
+
if (seen.has(key))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
out.push(tok);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the manifest the launch contract may be diffed against.
|
|
90
|
+
*
|
|
91
|
+
* A package.json that exists but does not parse resolves to `none`, not to "zero
|
|
92
|
+
* scripts": diffing against a manifest you could not read is exactly the mistake
|
|
93
|
+
* this module exists to stop, and a broken manifest is the static checks' business.
|
|
94
|
+
*/
|
|
95
|
+
export function readLaunchManifest(cwd) {
|
|
96
|
+
const pkg = path.join(cwd, 'package.json');
|
|
97
|
+
if (existsSync(pkg)) {
|
|
98
|
+
try {
|
|
99
|
+
const j = JSON.parse(readFileSync(pkg, 'utf8'));
|
|
100
|
+
return { kind: 'npm', file: 'package.json', names: Object.keys(j.scripts ?? {}) };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { kind: 'none', file: '', names: [], why: 'its package.json could not be parsed' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const name of MAKEFILE_NAMES) {
|
|
107
|
+
const mk = path.join(cwd, name);
|
|
108
|
+
if (!existsSync(mk))
|
|
109
|
+
continue;
|
|
110
|
+
try {
|
|
111
|
+
return { kind: 'make', file: name, names: makeTargets(readFileSync(mk, 'utf8')) };
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { kind: 'none', file: '', names: [], why: 'it has no npm manifest and no Makefile' };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The one line an INERT contract leaves behind. It rides in the gate's UNOBSERVED
|
|
121
|
+
* channel rather than in `warnings` for the reason nexttask 16A gives: a check that
|
|
122
|
+
* silently did nothing must not read later as a check that passed.
|
|
123
|
+
*/
|
|
124
|
+
export function inertLaunchContractNote(declared, manifest) {
|
|
125
|
+
return (`launch contract: ${declared.length} declared script(s) (${declared.join(', ')}) were recorded, `
|
|
126
|
+
+ `but this project could not be diffed against a manifest — ${manifest.why ?? 'no manifest'}. `
|
|
127
|
+
+ 'The contract was NOT checked here.');
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.5",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|