@mjasnikovs/pi-task 0.18.21 → 0.18.23
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/accept-debt.d.ts +45 -4
- package/dist/task/accept-debt.js +76 -9
- package/dist/task/final-gate.d.ts +2 -9
- package/dist/task/final-gate.js +10 -22
- package/dist/task/frozen-conflict.d.ts +36 -0
- package/dist/task/frozen-conflict.js +206 -0
- package/dist/task/frozen-path-guard.d.ts +9 -0
- package/dist/task/frozen-path-guard.js +15 -0
- package/dist/task/gate-deps.d.ts +8 -0
- package/dist/task/gate-deps.js +39 -3
- package/dist/task/lint-fix.d.ts +15 -1
- package/dist/task/lint-fix.js +82 -1
- package/dist/task/phases.d.ts +1 -1
- package/dist/task/phases.js +31 -9
- package/dist/task/prohibition-probe.d.ts +7 -0
- package/dist/task/prohibition-probe.js +1 -1
- package/dist/task/task-gates.d.ts +19 -0
- package/dist/task/task-gates.js +71 -8
- package/dist/task/task-provenance.d.ts +45 -0
- package/dist/task/task-provenance.js +98 -0
- package/dist/task/verify-work.d.ts +17 -1
- package/dist/task/verify-work.js +55 -4
- package/dist/task/write-guard.d.ts +10 -0
- package/dist/task/write-guard.js +40 -0
- package/package.json +1 -1
|
@@ -6,8 +6,19 @@
|
|
|
6
6
|
* server entry point … the Hono server cannot be started"), so the terminal defect
|
|
7
7
|
* was FOUND and then erased by the very mechanism that found it. Persisted here so
|
|
8
8
|
* the final gate re-checks and surfaces it instead of letting it die with the revert.
|
|
9
|
+
* - 'frozen-blocked' — a repo-health verify-FAIL whose only fix is an edit to a path
|
|
10
|
+
* THIS task's spec froze (mx5 run 12: `bun run lint` permanently red because the
|
|
11
|
+
* created files need a tsconfig registration every spec forbids). Cross-task
|
|
12
|
+
* contradiction: no unattended re-run can converge, so the gate loop records the
|
|
13
|
+
* defect and routes to the human picker instead of burning AUTOFIX rounds.
|
|
14
|
+
* - 'cross-task-deletion' — the task's work DELETED a sibling task's committed
|
|
15
|
+
* deliverable (mx5 run 12 PROMPT 2: a fix child deleted TASK_0020's playwright ct
|
|
16
|
+
* files to green a lint) and the user ACCEPTed the verify-FAIL anyway, so the
|
|
17
|
+
* deletion ships in the next commit. Recorded so the final gate re-checks it:
|
|
18
|
+
* resolved iff the named file is back in the tree (a later task restored it),
|
|
19
|
+
* otherwise surfaced.
|
|
9
20
|
*/
|
|
10
|
-
export type DebtOrigin = 'accepted' | 'enforce-revert';
|
|
21
|
+
export type DebtOrigin = 'accepted' | 'enforce-revert' | 'frozen-blocked' | 'cross-task-deletion';
|
|
11
22
|
/** One recorded defect: the task, why its VERIFY failed, and how it was recorded. */
|
|
12
23
|
export interface AcceptDebt {
|
|
13
24
|
taskId: string;
|
|
@@ -46,6 +57,33 @@ export declare function recordAcceptDebt(cwd: string, taskId: string, reason: st
|
|
|
46
57
|
* rather than letting it die with the revert.
|
|
47
58
|
*/
|
|
48
59
|
export declare function recordEnforceRevertDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Record a FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a repo-health FAIL
|
|
62
|
+
* whose static findings can only be fixed by editing a path this task's spec froze —
|
|
63
|
+
* a cross-task contradiction no unattended re-run may resolve. Recorded when the gate
|
|
64
|
+
* loop routes to the picker (regardless of what the human then picks), so the final
|
|
65
|
+
* gate re-checks it at run end. Static-class by reason prefix (`repo health: …`), so
|
|
66
|
+
* it auto-closes iff the final gate's own static check passes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function recordFrozenBlockedDebt(cwd: string, taskId: string, reason: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Record a CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's work deleted a
|
|
71
|
+
* file a DIFFERENT task's commit introduced, verify FAILed, and the user ACCEPTed —
|
|
72
|
+
* so the deletion survives into the next commit. The reason is a fixed machine-
|
|
73
|
+
* parseable shape (`deleted \`<path>\` …`) so the final gate's re-check can extract
|
|
74
|
+
* the path and prove the debt resolved iff the file is back in the tree.
|
|
75
|
+
*/
|
|
76
|
+
export declare function recordCrossTaskDeletionDebt(cwd: string, taskId: string, deletion: {
|
|
77
|
+
path: string;
|
|
78
|
+
owner: string;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
82
|
+
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
83
|
+
* unextractable path means the re-check cannot prove anything, so the debt
|
|
84
|
+
* stays open (surface, never re-hide).
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractDeletedDebtPath(reason: string): string | null;
|
|
49
87
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
50
88
|
export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Promise<void>;
|
|
51
89
|
/**
|
|
@@ -59,12 +97,15 @@ export declare function writeAcceptDebts(cwd: string, debts: AcceptDebt[]): Prom
|
|
|
59
97
|
export declare function isStaticClassDebt(reason: string): boolean;
|
|
60
98
|
/**
|
|
61
99
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
62
|
-
* iff the final gate's own static check now passes (`staticOk`);
|
|
63
|
-
*
|
|
64
|
-
*
|
|
100
|
+
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
101
|
+
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
102
|
+
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
103
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide). FP-safe: the only auto-closes are
|
|
104
|
+
* ones a deterministic check can stand behind.
|
|
65
105
|
*/
|
|
66
106
|
export declare function recheckAcceptDebts(debts: AcceptDebt[], opts: {
|
|
67
107
|
staticOk: boolean;
|
|
108
|
+
fileExists?: (rel: string) => boolean;
|
|
68
109
|
}): {
|
|
69
110
|
open: AcceptDebt[];
|
|
70
111
|
resolved: AcceptDebt[];
|
package/dist/task/accept-debt.js
CHANGED
|
@@ -74,7 +74,11 @@ export function parseAcceptDebts(raw) {
|
|
|
74
74
|
out.push({
|
|
75
75
|
taskId: parts[0].trim(),
|
|
76
76
|
reason: parts[1].trim(),
|
|
77
|
-
...(origin === 'enforce-revert'
|
|
77
|
+
...((origin === 'enforce-revert'
|
|
78
|
+
|| origin === 'frozen-blocked'
|
|
79
|
+
|| origin === 'cross-task-deletion') ?
|
|
80
|
+
{ origin: origin }
|
|
81
|
+
: {})
|
|
78
82
|
});
|
|
79
83
|
}
|
|
80
84
|
return out;
|
|
@@ -92,8 +96,8 @@ function normaliseReason(reason) {
|
|
|
92
96
|
}
|
|
93
97
|
function serialize(d) {
|
|
94
98
|
// Legacy 2-field shape for 'accepted' (backward compatible); a 3rd origin field
|
|
95
|
-
// only for the
|
|
96
|
-
return d.origin
|
|
99
|
+
// only for the non-accepted classes, so old readers/files round-trip unchanged.
|
|
100
|
+
return d.origin && d.origin !== 'accepted' ?
|
|
97
101
|
`${d.taskId}${FIELD_SEP}${d.reason}${FIELD_SEP}${d.origin}`
|
|
98
102
|
: `${d.taskId}${FIELD_SEP}${d.reason}`;
|
|
99
103
|
}
|
|
@@ -140,6 +144,45 @@ export async function recordEnforceRevertDebt(cwd, taskId, reason) {
|
|
|
140
144
|
origin: 'enforce-revert'
|
|
141
145
|
});
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Record a FROZEN-BLOCKED debt (mx5 run 12 / PROMPT 1 layer B): a repo-health FAIL
|
|
149
|
+
* whose static findings can only be fixed by editing a path this task's spec froze —
|
|
150
|
+
* a cross-task contradiction no unattended re-run may resolve. Recorded when the gate
|
|
151
|
+
* loop routes to the picker (regardless of what the human then picks), so the final
|
|
152
|
+
* gate re-checks it at run end. Static-class by reason prefix (`repo health: …`), so
|
|
153
|
+
* it auto-closes iff the final gate's own static check passes.
|
|
154
|
+
*/
|
|
155
|
+
export async function recordFrozenBlockedDebt(cwd, taskId, reason) {
|
|
156
|
+
await appendDebt(cwd, {
|
|
157
|
+
taskId: taskId.trim(),
|
|
158
|
+
reason: normaliseReason(reason),
|
|
159
|
+
origin: 'frozen-blocked'
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Record a CROSS-TASK DELETION debt (mx5 run 12 PROMPT 2): the task's work deleted a
|
|
164
|
+
* file a DIFFERENT task's commit introduced, verify FAILed, and the user ACCEPTed —
|
|
165
|
+
* so the deletion survives into the next commit. The reason is a fixed machine-
|
|
166
|
+
* parseable shape (`deleted \`<path>\` …`) so the final gate's re-check can extract
|
|
167
|
+
* the path and prove the debt resolved iff the file is back in the tree.
|
|
168
|
+
*/
|
|
169
|
+
export async function recordCrossTaskDeletionDebt(cwd, taskId, deletion) {
|
|
170
|
+
await appendDebt(cwd, {
|
|
171
|
+
taskId: taskId.trim(),
|
|
172
|
+
reason: normaliseReason(`deleted \`${deletion.path}\` — ${deletion.owner}'s committed deliverable, removed by this task's work`),
|
|
173
|
+
origin: 'cross-task-deletion'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The deleted path a cross-task-deletion debt names (the fixed shape
|
|
178
|
+
* recordCrossTaskDeletionDebt writes). Null on any other reason text — an
|
|
179
|
+
* unextractable path means the re-check cannot prove anything, so the debt
|
|
180
|
+
* stays open (surface, never re-hide).
|
|
181
|
+
*/
|
|
182
|
+
export function extractDeletedDebtPath(reason) {
|
|
183
|
+
const m = /^deleted `([^`]+)`/.exec(reason.trim());
|
|
184
|
+
return m ? m[1] : null;
|
|
185
|
+
}
|
|
143
186
|
/** Overwrite the ledger with exactly these records (used to prune resolved debts). */
|
|
144
187
|
export async function writeAcceptDebts(cwd, debts) {
|
|
145
188
|
try {
|
|
@@ -167,14 +210,31 @@ export function isStaticClassDebt(reason) {
|
|
|
167
210
|
}
|
|
168
211
|
/**
|
|
169
212
|
* Re-check the ledger against the current run state. A static-class debt is RESOLVED
|
|
170
|
-
* iff the final gate's own static check now passes (`staticOk`);
|
|
171
|
-
*
|
|
172
|
-
*
|
|
213
|
+
* iff the final gate's own static check now passes (`staticOk`); a cross-task-deletion
|
|
214
|
+
* debt is RESOLVED iff the file it names is back in the tree (`fileExists` — a later
|
|
215
|
+
* task or a human restored it, so the deletion no longer holds); every other debt
|
|
216
|
+
* stays OPEN (unprovable ⇒ surface, never re-hide). FP-safe: the only auto-closes are
|
|
217
|
+
* ones a deterministic check can stand behind.
|
|
173
218
|
*/
|
|
174
219
|
export function recheckAcceptDebts(debts, opts) {
|
|
175
220
|
const open = [];
|
|
176
221
|
const resolved = [];
|
|
177
222
|
for (const d of debts) {
|
|
223
|
+
if (d.origin === 'cross-task-deletion') {
|
|
224
|
+
const p = extractDeletedDebtPath(d.reason);
|
|
225
|
+
let restored;
|
|
226
|
+
try {
|
|
227
|
+
restored = p !== null && opts.fileExists?.(p) === true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
restored = false; // an existence-check fault is inconclusive, not proof
|
|
231
|
+
}
|
|
232
|
+
if (restored)
|
|
233
|
+
resolved.push(d);
|
|
234
|
+
else
|
|
235
|
+
open.push(d);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
178
238
|
if (opts.staticOk && isStaticClassDebt(d.reason))
|
|
179
239
|
resolved.push(d);
|
|
180
240
|
else
|
|
@@ -255,7 +315,14 @@ export function buildAcceptDebtNote(open) {
|
|
|
255
315
|
}
|
|
256
316
|
/** One-line provenance label for a debt, for the surfaced report. */
|
|
257
317
|
export function describeDebt(d) {
|
|
258
|
-
|
|
259
|
-
'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)'
|
|
260
|
-
|
|
318
|
+
if (d.origin === 'enforce-revert') {
|
|
319
|
+
return 'enforce re-verify FAILED then the edits were reverted (defect indicts the ORIGINAL work, still shipped)';
|
|
320
|
+
}
|
|
321
|
+
if (d.origin === 'frozen-blocked') {
|
|
322
|
+
return 'repo health blocked by a spec-frozen path (cross-task contradiction — no task may perform the fixing edit)';
|
|
323
|
+
}
|
|
324
|
+
if (d.origin === 'cross-task-deletion') {
|
|
325
|
+
return "a sibling task's committed deliverable was DELETED by this task's work and the deletion was accepted (still missing from the tree)";
|
|
326
|
+
}
|
|
327
|
+
return 'accepted despite verify-FAIL';
|
|
261
328
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type HealthCommand } from './repo-health-check.js';
|
|
2
2
|
import { type AcceptDebt } from './accept-debt.js';
|
|
3
3
|
import { type RenderOutcome } from './render-check.js';
|
|
4
|
+
import { taskThatIntroduced } from './task-provenance.js';
|
|
4
5
|
export interface FinalGateOutcome {
|
|
5
6
|
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
6
7
|
ok: boolean;
|
|
@@ -140,14 +141,7 @@ export declare function discoverGateCommandLabels(cwd: string): string[];
|
|
|
140
141
|
* this box; the same wording in a `test` run is a real failure the suite must own).
|
|
141
142
|
*/
|
|
142
143
|
export declare const INFRA_GAP_OUTPUT_RE: RegExp;
|
|
143
|
-
|
|
144
|
-
* The task whose commit INTRODUCED `rel` (oldest `--diff-filter=A` commit whose
|
|
145
|
-
* subject carries the pi-task `(TASK_nnnn)` suffix — both the task snapshot and the
|
|
146
|
-
* ENFORCE commit shapes match). Null when the file predates the run, was never
|
|
147
|
-
* committed, git is unavailable, or the adding commit is not a task commit — every
|
|
148
|
-
* unknown degrades to "no conflict claim".
|
|
149
|
-
*/
|
|
150
|
-
export declare function taskThatIntroduced(cwd: string, rel: string): string | null;
|
|
144
|
+
export { taskThatIntroduced };
|
|
151
145
|
/**
|
|
152
146
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
153
147
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -155,4 +149,3 @@ export declare function taskThatIntroduced(cwd: string, rel: string): string | n
|
|
|
155
149
|
* First real failure wins.
|
|
156
150
|
*/
|
|
157
151
|
export declare function runFinalIntegrationGate(cwd: string, timeoutMs?: number, bootGraceMs?: number, bootDeps?: BootDeps, planText?: string): Promise<FinalGateOutcome>;
|
|
158
|
-
export {};
|
package/dist/task/final-gate.js
CHANGED
|
@@ -46,6 +46,7 @@ import { readAcceptDebts, recheckAcceptDebts, writeAcceptDebts, buildAcceptDebtN
|
|
|
46
46
|
import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts } from './launch-contract.js';
|
|
47
47
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
48
48
|
import { runRenderCheck } from './render-check.js';
|
|
49
|
+
import { taskThatIntroduced } from './task-provenance.js';
|
|
49
50
|
function packageScripts(cwd) {
|
|
50
51
|
try {
|
|
51
52
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -586,27 +587,10 @@ async function recoverOrphanPort(cwd, boot, first, bootGraceMs, deps, expectServ
|
|
|
586
587
|
await new Promise(r => setTimeout(r, 1_500));
|
|
587
588
|
return runBootCheck(cwd, boot, bootGraceMs, { expectServer, deps });
|
|
588
589
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
* committed, git is unavailable, or the adding commit is not a task commit — every
|
|
594
|
-
* unknown degrades to "no conflict claim".
|
|
595
|
-
*/
|
|
596
|
-
export function taskThatIntroduced(cwd, rel) {
|
|
597
|
-
const r = spawnSync('git', ['log', '--diff-filter=A', '--format=%s', '--', rel], {
|
|
598
|
-
cwd,
|
|
599
|
-
encoding: 'utf8'
|
|
600
|
-
});
|
|
601
|
-
if (r.error || r.status !== 0 || !r.stdout)
|
|
602
|
-
return null;
|
|
603
|
-
const subjects = r.stdout.trim().split('\n').filter(Boolean);
|
|
604
|
-
// Newest-first output; the LAST line is the original introduction (a
|
|
605
|
-
// delete-and-re-add later in history must not reattribute the file).
|
|
606
|
-
const first = subjects[subjects.length - 1] ?? '';
|
|
607
|
-
const m = /\((TASK_\d+)\)\s*$/.exec(first);
|
|
608
|
-
return m ? m[1] : null;
|
|
609
|
-
}
|
|
590
|
+
// File → introducing-task provenance moved to task-provenance.ts (mx5 run-12
|
|
591
|
+
// PROMPT 2 extracted it for the cross-task deletion guards); re-exported so
|
|
592
|
+
// existing importers keep working.
|
|
593
|
+
export { taskThatIntroduced };
|
|
610
594
|
/**
|
|
611
595
|
* Run the final gate: static analysis first, then the lockfile consistency
|
|
612
596
|
* checks, then the discovered integration commands, then one boot exercise of
|
|
@@ -624,7 +608,11 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
624
608
|
// construction (see accept-debt.ts). Best-effort: a ledger read/write failure
|
|
625
609
|
// must never break the gate.
|
|
626
610
|
const { open: openRaw, resolved } = recheckAcceptDebts(await readAcceptDebts(cwd), {
|
|
627
|
-
staticOk: stat.ok
|
|
611
|
+
staticOk: stat.ok,
|
|
612
|
+
// Cross-task-deletion debts auto-close iff the deleted file is back in the
|
|
613
|
+
// tree — a deterministic existence check, corroborating the per-file
|
|
614
|
+
// provenance the record already carries.
|
|
615
|
+
fileExists: rel => existsSync(path.join(cwd, rel))
|
|
628
616
|
});
|
|
629
617
|
if (resolved.length > 0)
|
|
630
618
|
await writeAcceptDebts(cwd, openRaw);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One unsatisfiable freeze/requires-edit pair found in a composed spec. */
|
|
2
|
+
export interface FrozenPathConflict {
|
|
3
|
+
/** The frozen path (normalized: no leading ./, no trailing /). */
|
|
4
|
+
path: string;
|
|
5
|
+
/** The freeze line, verbatim (the `Do NOT modify` constraint). */
|
|
6
|
+
constraint: string;
|
|
7
|
+
/** The sentence stating the deliverable requires changing that path
|
|
8
|
+
* (or surrendering to the consequences of not being allowed to). */
|
|
9
|
+
statement: string;
|
|
10
|
+
/** Where the statement was found: the spec's own body, or the task's
|
|
11
|
+
* research (the compose INPUT — live drafts sometimes drop the research
|
|
12
|
+
* nuance from the spec text while the contradiction remains real). */
|
|
13
|
+
source: 'spec' | 'research';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find every unsatisfiable pair in the composed spec: a BLANKET frozen path
|
|
17
|
+
* whose registration/edit is declared necessary by the spec's own body (or
|
|
18
|
+
* explicitly surrendered to), or - when `research` is given - by the task's
|
|
19
|
+
* research the spec was composed FROM. The frozen paths always come from the
|
|
20
|
+
* SPEC alone; research contributes only the statement side (live compose
|
|
21
|
+
* sometimes drops the research's "must also be included" nuance from the spec
|
|
22
|
+
* text while shipping the freeze and the file creation - the contradiction is
|
|
23
|
+
* then visible only across the compose boundary). Deterministic, pure text -
|
|
24
|
+
* the same extraction the live A/B grounds its measurements in, so the model
|
|
25
|
+
* cannot self-report its way past it. Empty on a null spec or one that froze
|
|
26
|
+
* nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findFrozenPathConflicts(spec: string | null | undefined, research?: string | null): FrozenPathConflict[];
|
|
29
|
+
/**
|
|
30
|
+
* The forced critique-rewrite defect text (skip-escape pattern: MANDATORY,
|
|
31
|
+
* self-contained, names the exact resolution options). The two permitted
|
|
32
|
+
* resolutions come straight from the incident analysis: scoped ownership or
|
|
33
|
+
* dropping the creation — prose acknowledging the gap is called out as a
|
|
34
|
+
* non-resolution because that is precisely what the live model shipped.
|
|
35
|
+
*/
|
|
36
|
+
export declare function frozenConflictProbeText(conflicts: FrozenPathConflict[]): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frozen-conflict — deterministic detection of an UNSATISFIABLE spec pair at
|
|
3
|
+
* compose time (mx5 run 12 root cause, PROMPT 1 layer A).
|
|
4
|
+
*
|
|
5
|
+
* The failure this closes: compose authored TASK_0020 with a blanket freeze
|
|
6
|
+
* ("Do not modify `tsconfig.json`, `eslint.config.js`, or `.prettierrc.cjs`;
|
|
7
|
+
* those are handled in steps 1–2") while the deliverable — new
|
|
8
|
+
* `playwright-ct.config.ts` + `playwright/index.ts` — needs exactly that
|
|
9
|
+
* registration edit, and the spec "resolved" the contradiction with prose
|
|
10
|
+
* surrender ("Accept that `playwright-ct.config.ts` will not be covered by
|
|
11
|
+
* `tsc --noEmit` since `tsconfig.json` is not modified in this step"). Typed
|
|
12
|
+
* ESLint does not "accept" anything: the moment the created files land,
|
|
13
|
+
* `bun run lint` hard-errors REPO-WIDE and permanently — and the "owning"
|
|
14
|
+
* steps 1–2 completed long ago, so NO task may ever perform the edit (every
|
|
15
|
+
* spec carries the same freeze). Every subsequent task then burned its
|
|
16
|
+
* unattended AUTOFIX rounds on a repo-health FAIL none of them was allowed to
|
|
17
|
+
* fix, and the eventual "escape" was a child DELETING the deliverables.
|
|
18
|
+
*
|
|
19
|
+
* The contradiction is visible IN THE COMPOSED TEXT ITSELF, so it must die at
|
|
20
|
+
* spec time. Like the skip-escape / synthesized-wiring / plan-contradiction
|
|
21
|
+
* probes (prompt-only rules are A/B-proven ~0–1/5 on the weak model), the
|
|
22
|
+
* detector is deterministic and its finding is FORCED into the critique
|
|
23
|
+
* rewrite: the rewrite must either grant scoped ownership ("MAY edit `X` ONLY
|
|
24
|
+
* to register the files this task creates") or drop the file creation.
|
|
25
|
+
*
|
|
26
|
+
* High-precision by construction (FP-swept over the 26 real mx5 run-12 specs):
|
|
27
|
+
* - the freeze side must be BLANKET — a `Do NOT modify` line with no
|
|
28
|
+
* exception clause; a scoped freeze ("MAY edit … ONLY to register",
|
|
29
|
+
* "Only edit `X` …", "… except to add …") is already the resolution shape
|
|
30
|
+
* and must never re-fire;
|
|
31
|
+
* - the statement side is judged per SENTENCE, not per line (real GOALs are
|
|
32
|
+
* one giant line — line scoping false-fired on `must include` in a
|
|
33
|
+
* response-shape sentence three paths away from the frozen one);
|
|
34
|
+
* - a sentence that is itself prohibition-shaped ("Preserve … do not
|
|
35
|
+
* remove or modify …") is the freeze side restated, never a statement;
|
|
36
|
+
* - the sentence must NAME the frozen path (pathNamedIn) AND match one of
|
|
37
|
+
* the measured phrasing families: passive registration ("must also be
|
|
38
|
+
* included"), unless-added ("won't be type-checked unless added"),
|
|
39
|
+
* directional active ("requires adding … to" / "must add … to"), or the
|
|
40
|
+
* prose SURRENDER itself ("will not be covered … since `X` is not
|
|
41
|
+
* modified" — the exact line the live TASK_0020 spec shipped).
|
|
42
|
+
* Mere co-mention never fires.
|
|
43
|
+
*/
|
|
44
|
+
import { extractProhibitions, PROHIBITION_RE } from './prohibition-probe.js';
|
|
45
|
+
import { pathNamedIn } from './frozen-path-guard.js';
|
|
46
|
+
/**
|
|
47
|
+
* A freeze that carves out its own exception is SCOPED, not blanket — it is
|
|
48
|
+
* exactly the resolution shape the probe demands ("MAY edit `X` ONLY to
|
|
49
|
+
* register…", "Only edit `X` (add the search route)…", "Do not modify `X`
|
|
50
|
+
* except to add…"), so it never counts as the freeze side of a conflict, or
|
|
51
|
+
* the recomposed spec would re-fire forever.
|
|
52
|
+
*/
|
|
53
|
+
const EXCEPTION_RE = /\b(?:except|unless|beyond|other\s+than|apart\s+from|only\s+(?:to|for|if|when|where|edit|modify|change|touch|update)|may\s+(?:edit|modify|change|update|add))\b/i;
|
|
54
|
+
/** Passive registration: "must (also) be included/added/registered/…". */
|
|
55
|
+
const PASSIVE_REG_RE = /\b(?:must|needs?\s+to|has\s+to|should)\s+(?:also\s+)?be\s+(?:includ|add|regist|list|referenc|declar|updat)\w*/i;
|
|
56
|
+
/** "unless (it is) added/included/registered/updated". */
|
|
57
|
+
const UNLESS_ADDED_RE = /\bunless\s+(?:it\s+is\s+|it'?s\s+|they\s+are\s+|first\s+)?(?:add|includ|regist|updat)\w*/i;
|
|
58
|
+
/** "won't be type-checked/compiled/linted/covered … unless …". */
|
|
59
|
+
const NOT_CHECKED_UNLESS_RE = /\b(?:won'?t|will\s+not|cannot|can'?t|does\s+not|doesn'?t|is\s+not|isn'?t)\s+(?:be\s+)?[\w\s-]{0,40}?(?:type-?check|compil|lint|cover|resolv|recogni[sz]|includ|pick)\w*[^;]{0,80}?\bunless\b/i;
|
|
60
|
+
/** Directional active: "requires adding … to …" / "must add … to/into/in …".
|
|
61
|
+
* The preposition requirement is what keeps a response-shape "must include
|
|
62
|
+
* `field`" sentence from counting as a registration edit. */
|
|
63
|
+
const REQUIRES_DIRECTIONAL_RE = /\brequires?\s+(?:add|includ|regist|updat|edit|modify|chang)\w*[^;]{0,80}?\b(?:to|into|in)\b/i;
|
|
64
|
+
const MUST_ADD_DIRECTIONAL_RE = /\bmust\s+(?:also\s+)?(?:add|includ|regist|updat)\w*[^;]{0,80}?\b(?:to|into|in)\b/i;
|
|
65
|
+
/** The prose-surrender pair: an artifact "will not be covered/type-checked/…"
|
|
66
|
+
* BECAUSE the (frozen) path "is not modified". Both halves must be present —
|
|
67
|
+
* this is the exact contradiction shape the live TASK_0020 spec shipped. */
|
|
68
|
+
const NOT_COVERED_RE = /\b(?:will\s+not|won'?t|cannot|can'?t|is\s+not|isn'?t)\s+(?:be\s+)?(?:covered|type-?checked|checked|compiled|linted|validated|included)\b/i;
|
|
69
|
+
const BECAUSE_NOT_MODIFIED_RE = /\b(?:since|because|as)\b[^;]{0,80}?\b(?:is\s+|are\s+|was\s+|were\s+)?not\s+(?:be(?:ing)?\s+)?(?:modif|edit|chang|updat|touch)\w*/i;
|
|
70
|
+
function isRequiresEditStatement(sentence) {
|
|
71
|
+
return (PASSIVE_REG_RE.test(sentence)
|
|
72
|
+
|| UNLESS_ADDED_RE.test(sentence)
|
|
73
|
+
|| NOT_CHECKED_UNLESS_RE.test(sentence)
|
|
74
|
+
|| REQUIRES_DIRECTIONAL_RE.test(sentence)
|
|
75
|
+
|| MUST_ADD_DIRECTIONAL_RE.test(sentence)
|
|
76
|
+
|| (NOT_COVERED_RE.test(sentence) && BECAUSE_NOT_MODIFIED_RE.test(sentence)));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Split a spec line into sentences: a `.` or `;` followed by whitespace and a
|
|
80
|
+
* capital/backtick/bracket opener ends a sentence. The opener requirement
|
|
81
|
+
* keeps `e.g. foo` and mid-path dots intact; backtick paths carry no `. ` so
|
|
82
|
+
* they never split. Judging per sentence is what makes one-giant-line GOALs
|
|
83
|
+
* scannable without cross-sentence false fires.
|
|
84
|
+
*/
|
|
85
|
+
function splitSentences(line) {
|
|
86
|
+
return line.split(/[.;]\s+(?=[A-Z`([-])/);
|
|
87
|
+
}
|
|
88
|
+
/** Backtick-quoted path-shaped tokens in a sentence (the same shape rule the
|
|
89
|
+
* prohibition extractor applies), minus surrounding quotes. */
|
|
90
|
+
function pathTokensIn(sentence) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const m of sentence.matchAll(/`([^`]+)`/g)) {
|
|
93
|
+
const token = m[1].trim().replace(/^["']|["']$/g, '');
|
|
94
|
+
if (!/^[\w.@~/-]+$/.test(token))
|
|
95
|
+
continue;
|
|
96
|
+
if (token.includes('/') || /\.[A-Za-z0-9]+$/.test(token) || token.startsWith('.')) {
|
|
97
|
+
out.push(token);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Scan one text's sentences for requires-edit statements naming a blanket
|
|
103
|
+
* frozen path, appending each new pair to `out`.
|
|
104
|
+
*
|
|
105
|
+
* `anchorSpec` (research scans only): a research statement counts ONLY when,
|
|
106
|
+
* besides the frozen path, it names at least one other path that still
|
|
107
|
+
* appears in the SPEC. That anchor is what makes resolution (b) terminal:
|
|
108
|
+
* when the rewrite DROPS the file creation, the created file vanishes from
|
|
109
|
+
* the spec, the research statement about it loses its anchor, and the
|
|
110
|
+
* detector goes quiet instead of re-firing forever on stale research. */
|
|
111
|
+
function scanForStatements(text, blanket, source, out, seen, anchorSpec) {
|
|
112
|
+
for (const raw of text.split('\n')) {
|
|
113
|
+
for (const fragment of splitSentences(raw)) {
|
|
114
|
+
const sentence = fragment.trim();
|
|
115
|
+
if (sentence.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
// A prohibition-shaped sentence IS the freeze side (the constraint
|
|
118
|
+
// itself, or a "Preserve/do not remove or modify" restatement) -
|
|
119
|
+
// never the requires-edit side.
|
|
120
|
+
if (PROHIBITION_RE.test(sentence))
|
|
121
|
+
continue;
|
|
122
|
+
if (!isRequiresEditStatement(sentence))
|
|
123
|
+
continue;
|
|
124
|
+
for (const p of blanket) {
|
|
125
|
+
if (!pathNamedIn(sentence, p.path))
|
|
126
|
+
continue;
|
|
127
|
+
if (anchorSpec !== undefined) {
|
|
128
|
+
const anchors = pathTokensIn(sentence).filter(t => t !== p.path && !pathNamedIn(p.path, t));
|
|
129
|
+
if (!anchors.some(a => pathNamedIn(anchorSpec, a)))
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const key = `${p.path} ${sentence}`;
|
|
133
|
+
if (seen.has(key))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
out.push({ path: p.path, constraint: p.constraint, statement: sentence, source });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Find every unsatisfiable pair in the composed spec: a BLANKET frozen path
|
|
143
|
+
* whose registration/edit is declared necessary by the spec's own body (or
|
|
144
|
+
* explicitly surrendered to), or - when `research` is given - by the task's
|
|
145
|
+
* research the spec was composed FROM. The frozen paths always come from the
|
|
146
|
+
* SPEC alone; research contributes only the statement side (live compose
|
|
147
|
+
* sometimes drops the research's "must also be included" nuance from the spec
|
|
148
|
+
* text while shipping the freeze and the file creation - the contradiction is
|
|
149
|
+
* then visible only across the compose boundary). Deterministic, pure text -
|
|
150
|
+
* the same extraction the live A/B grounds its measurements in, so the model
|
|
151
|
+
* cannot self-report its way past it. Empty on a null spec or one that froze
|
|
152
|
+
* nothing.
|
|
153
|
+
*/
|
|
154
|
+
export function findFrozenPathConflicts(spec, research) {
|
|
155
|
+
if (!spec)
|
|
156
|
+
return [];
|
|
157
|
+
const blanket = extractProhibitions(spec)
|
|
158
|
+
.filter(p => !EXCEPTION_RE.test(p.constraint))
|
|
159
|
+
.map(p => ({
|
|
160
|
+
path: p.path.replace(/^\.\//, '').replace(/\/+$/, ''),
|
|
161
|
+
constraint: p.constraint
|
|
162
|
+
}))
|
|
163
|
+
.filter(p => p.path.length > 0);
|
|
164
|
+
if (blanket.length === 0)
|
|
165
|
+
return [];
|
|
166
|
+
const out = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
scanForStatements(spec, blanket, 'spec', out, seen);
|
|
169
|
+
if (research)
|
|
170
|
+
scanForStatements(research, blanket, 'research', out, seen, spec);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The forced critique-rewrite defect text (skip-escape pattern: MANDATORY,
|
|
175
|
+
* self-contained, names the exact resolution options). The two permitted
|
|
176
|
+
* resolutions come straight from the incident analysis: scoped ownership or
|
|
177
|
+
* dropping the creation — prose acknowledging the gap is called out as a
|
|
178
|
+
* non-resolution because that is precisely what the live model shipped.
|
|
179
|
+
*/
|
|
180
|
+
export function frozenConflictProbeText(conflicts) {
|
|
181
|
+
const items = conflicts.map(c => `- the spec FREEZES \`${c.path}\` ("${c.constraint.slice(0, 160)}") yet `
|
|
182
|
+
+ (c.source === 'research' ?
|
|
183
|
+
`the task's own RESEARCH (the input this spec was composed from) states the `
|
|
184
|
+
+ `deliverable REQUIRES changing it ("${c.statement.slice(0, 160)}") — the spec `
|
|
185
|
+
+ `omitting this fact does not remove the requirement`
|
|
186
|
+
: `its own body states the deliverable REQUIRES changing it `
|
|
187
|
+
+ `("${c.statement.slice(0, 160)}")`));
|
|
188
|
+
return [
|
|
189
|
+
'UNSATISFIABLE-CONSTRAINT FINDING (deterministic; MUST be resolved, it overrides a CLEAN triage):',
|
|
190
|
+
...items,
|
|
191
|
+
'This pair is self-contradictory: the files this task creates need a registration edit',
|
|
192
|
+
'that the spec itself forbids, and the step that "owns" the frozen file has already',
|
|
193
|
+
'completed — no task will ever be allowed to perform the edit. The moment the created',
|
|
194
|
+
'files land, the repo-wide static check fails PERMANENTLY and every later task burns',
|
|
195
|
+
'its autofix rounds on a defect none of them may touch. Prose acknowledging the gap',
|
|
196
|
+
'("accept that it will not be covered…") is NOT a resolution.',
|
|
197
|
+
'REWRITE the spec to resolve it in exactly ONE of these two ways:',
|
|
198
|
+
' (a) SCOPED OWNERSHIP — replace the blanket freeze on the conflicting path with:',
|
|
199
|
+
' "You MAY edit `<path>` ONLY to register the files this task creates (e.g. add',
|
|
200
|
+
' them to its include list); any other change to `<path>` is forbidden." Keep the',
|
|
201
|
+
' blanket freeze for the other frozen paths.',
|
|
202
|
+
' (b) DROP the creation of the files that would require the frozen edit (and remove',
|
|
203
|
+
' the acceptance/verify steps that depend on them), stating why.',
|
|
204
|
+
'Never ship both the blanket freeze and the requires-edit statement.'
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
@@ -11,6 +11,15 @@ export type FrozenGit = (args: string[]) => Promise<{
|
|
|
11
11
|
* no-op by construction.
|
|
12
12
|
*/
|
|
13
13
|
export declare function frozenPathsFromSpec(spec: string | null | undefined): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Does this prose/tool-output text NAME the given path? Word-bounded on both
|
|
16
|
+
* sides so `tsconfig.json` matches `` `tsconfig.json` ``, `(tsconfig.json:18)`
|
|
17
|
+
* and a bare mention, but never `foo.tsconfig.json`, `config/tsconfig.json`
|
|
18
|
+
* (a different file) or `tsconfig.json5`/`tsconfig.json.bak`. Shared by the
|
|
19
|
+
* compose-time unsatisfiable-pair detector (frozen-conflict.ts) and lint-fix's
|
|
20
|
+
* non-convergence trace, so "the text names a frozen path" means one thing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pathNamedIn(text: string, path: string): boolean;
|
|
14
23
|
/**
|
|
15
24
|
* Parse `git status --porcelain` output (already scoped to the frozen pathspec)
|
|
16
25
|
* into the list of changed files, for the gate-trail record and to decide whether
|
|
@@ -48,6 +48,21 @@ export function frozenPathsFromSpec(spec) {
|
|
|
48
48
|
}
|
|
49
49
|
return [...seen];
|
|
50
50
|
}
|
|
51
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
/**
|
|
53
|
+
* Does this prose/tool-output text NAME the given path? Word-bounded on both
|
|
54
|
+
* sides so `tsconfig.json` matches `` `tsconfig.json` ``, `(tsconfig.json:18)`
|
|
55
|
+
* and a bare mention, but never `foo.tsconfig.json`, `config/tsconfig.json`
|
|
56
|
+
* (a different file) or `tsconfig.json5`/`tsconfig.json.bak`. Shared by the
|
|
57
|
+
* compose-time unsatisfiable-pair detector (frozen-conflict.ts) and lint-fix's
|
|
58
|
+
* non-convergence trace, so "the text names a frozen path" means one thing.
|
|
59
|
+
*/
|
|
60
|
+
export function pathNamedIn(text, path) {
|
|
61
|
+
const p = path.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
62
|
+
if (p.length === 0)
|
|
63
|
+
return false;
|
|
64
|
+
return new RegExp(`(?:^|[^\\w./-])${escapeRe(p)}(?!\\.?[\\w-])`, 'im').test(text);
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* Parse `git status --porcelain` output (already scoped to the frozen pathspec)
|
|
53
68
|
* into the list of changed files, for the gate-trail record and to decide whether
|
package/dist/task/gate-deps.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Pr
|
|
|
40
40
|
* Failures degrade to an empty summary — the guard then has nothing to reject.
|
|
41
41
|
*/
|
|
42
42
|
export declare function collectTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
43
|
+
/**
|
|
44
|
+
* The task's changes for the cross-task deletion probe: the working tree's status
|
|
45
|
+
* when the work is uncommitted (pre-commit verify), else the LAST COMMIT's
|
|
46
|
+
* name-status diff (the post-enforce re-verify runs on a clean tree, where that
|
|
47
|
+
* commit IS the task's work). Failures degrade to an empty summary — the probe is
|
|
48
|
+
* a sharpener, never a blocker. Same fallback discipline as collectChangedFiles.
|
|
49
|
+
*/
|
|
50
|
+
export declare function collectTaskTreeChanges(cwd: string, signal?: AbortSignal): Promise<TreeChangeSummary>;
|
|
43
51
|
/**
|
|
44
52
|
* Build the gate deps for one command run. `runTask` is the orchestrator's
|
|
45
53
|
* implementation re-runner, injected by the caller. The returned object also drives
|