tickmarkr 1.87.0 → 1.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/catalog.d.ts +18 -1
- package/dist/adapters/catalog.js +44 -1
- package/dist/adapters/fake.d.ts +2 -1
- package/dist/adapters/fake.js +7 -0
- package/dist/adapters/grok.js +11 -0
- package/dist/adapters/kimi.d.ts +2 -1
- package/dist/adapters/kimi.js +36 -0
- package/dist/adapters/opencode.js +17 -0
- package/dist/adapters/pi.js +11 -0
- package/dist/adapters/prompt.js +8 -1
- package/dist/adapters/registry.js +76 -57
- package/dist/adapters/types.d.ts +34 -3
- package/dist/adapters/types.js +99 -1
- package/dist/cli/commands/approve.d.ts +2 -0
- package/dist/cli/commands/approve.js +104 -84
- package/dist/cli/commands/compile.d.ts +1 -1
- package/dist/cli/commands/compile.js +29 -12
- package/dist/cli/commands/doctor.d.ts +16 -0
- package/dist/cli/commands/doctor.js +52 -0
- package/dist/cli/commands/init.js +2 -1
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +10 -1
- package/dist/cli/commands/report.js +49 -0
- package/dist/cli/commands/status.js +298 -96
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.js +177 -0
- package/dist/cli/harness.d.ts +13 -0
- package/dist/cli/harness.js +50 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/collateral.js +11 -11
- package/dist/compile/common.js +2 -2
- package/dist/compile/index.d.ts +14 -3
- package/dist/compile/index.js +36 -10
- package/dist/compile/native.d.ts +15 -1
- package/dist/compile/native.js +310 -28
- package/dist/config/config.js +2 -2
- package/dist/drivers/subprocess.d.ts +6 -1
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +21 -1
- package/dist/gates/acceptance.js +67 -22
- package/dist/gates/artifact-manifest.d.ts +119 -0
- package/dist/gates/artifact-manifest.js +357 -0
- package/dist/gates/baseline.d.ts +45 -5
- package/dist/gates/baseline.js +119 -15
- package/dist/gates/llm.js +37 -26
- package/dist/gates/review.d.ts +16 -11
- package/dist/gates/review.js +44 -150
- package/dist/gates/run-gates.js +124 -7
- package/dist/gates/scope.js +3 -3
- package/dist/graph/files-glob.d.ts +18 -0
- package/dist/graph/files-glob.js +22 -0
- package/dist/graph/schema.d.ts +3 -1
- package/dist/graph/schema.js +4 -1
- package/dist/run/daemon.d.ts +44 -0
- package/dist/run/daemon.js +2334 -1973
- package/dist/run/git.d.ts +53 -0
- package/dist/run/git.js +119 -5
- package/dist/run/interactive-seed.d.ts +6 -2
- package/dist/run/interactive-seed.js +72 -5
- package/dist/run/journal.d.ts +9 -1
- package/dist/run/journal.js +99 -9
- package/dist/run/lock.d.ts +11 -0
- package/dist/run/lock.js +97 -6
- package/dist/run/merge.d.ts +4 -1
- package/dist/run/merge.js +26 -7
- package/dist/run/outcome.d.ts +50 -0
- package/dist/run/outcome.js +152 -0
- package/dist/run/protocol.d.ts +460 -0
- package/dist/run/protocol.js +433 -0
- package/dist/run/supervision.d.ts +29 -0
- package/dist/run/supervision.js +189 -0
- package/fixtures/authoring-lints/01-awk-range-self-pass.spec.md +12 -0
- package/fixtures/authoring-lints/02-judge-text-key-miss.spec.md +7 -0
- package/fixtures/authoring-lints/03-c1-t41-rendered-observable.spec.md +8 -0
- package/fixtures/authoring-lints/04-c1-t24-named-file.spec.md +8 -0
- package/fixtures/authoring-lints/05-c2-t24-t28-dep-inversion.spec.md +7 -0
- package/fixtures/authoring-lints/06-c2-denumbered-coupling.spec.md +7 -0
- package/fixtures/authoring-lints/07-c3a-t41-line-count-proxy.spec.md +7 -0
- package/fixtures/authoring-lints/08-c3b-t41-governance-referent.spec.md +7 -0
- package/fixtures/authoring-lints/09-c4-universals-without-pointer.spec.md +7 -0
- package/fixtures/authoring-lints/10-c5-t34-conjunct-flood.spec.md +7 -0
- package/fixtures/authoring-lints/11-c6-t34-q3-q9-q20-bundle.spec.md +7 -0
- package/fixtures/authoring-lints/12-c7-t24-prose-seam.spec.md +8 -0
- package/fixtures/wrapped-acceptance.native.md +29 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +21 -2
- package/skills/tickmarkr-overseer/SKILL.md +262 -5
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +79 -8
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +80 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +86 -0
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +96 -0
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +201 -0
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { channelKey, shq } from "../adapters/types.js";
|
|
5
|
+
import { compileNative } from "../compile/native.js";
|
|
3
6
|
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
4
7
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
5
8
|
import { sh } from "../run/git.js";
|
|
6
|
-
import {
|
|
9
|
+
import { checkTaskDiffCaps, fetchTaskDiff, isProtectedEvidence, setAsideReceiptPath } from "./review.js";
|
|
7
10
|
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
|
|
8
11
|
import { classifyVerdictCause } from "./verdict-cause.js";
|
|
12
|
+
import { reviewableLogicDiff } from "./artifact-manifest.js";
|
|
9
13
|
// Fable F4: acceptance judge shares review's 900s timeout — 300s default killed frontier judges on cap-sized diffs.
|
|
10
14
|
const JUDGE_TIMEOUT_MS = 900_000;
|
|
11
15
|
const CitationSchema = z.object({ path: z.string(), line: z.number().int() });
|
|
@@ -70,16 +74,75 @@ const isJudge = (a) => typeof a === "string" || (typeof a === "object" && a.orac
|
|
|
70
74
|
function escapeRegExp(s) {
|
|
71
75
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
76
|
}
|
|
77
|
+
// v1.89 T6: Vitest applies -t to the runner-visible full name (enclosing describe titles and the test
|
|
78
|
+
// title, space-joined). Escaping preserves literal criteria; anchoring makes equality, rather than
|
|
79
|
+
// substring containment, the named-test contract.
|
|
80
|
+
export function testFilterPattern(name) {
|
|
81
|
+
return `^${escapeRegExp(name)}$`;
|
|
82
|
+
}
|
|
73
83
|
// OBS-55: when the base command already contains `--`, append -t after forwarded args — a second `--`
|
|
74
84
|
// makes vitest treat -t as a positional file filter and the name filter is dropped.
|
|
75
85
|
export function testFiltered(testCmd, name) {
|
|
76
|
-
const pattern =
|
|
86
|
+
const pattern = testFilterPattern(name);
|
|
77
87
|
const wrapped = /^\s*(npm|yarn|pnpm|npx)\b/.test(testCmd);
|
|
78
88
|
if (wrapped && /\s--\s/.test(testCmd))
|
|
79
89
|
return `${testCmd} -t ${shq(pattern)}`;
|
|
80
90
|
const fwd = wrapped ? "-- " : "";
|
|
81
91
|
return `${testCmd} ${fwd}-t ${shq(pattern)}`;
|
|
82
92
|
}
|
|
93
|
+
function corpusSpecPaths(root) {
|
|
94
|
+
const paths = [];
|
|
95
|
+
const visit = (dir) => {
|
|
96
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
97
|
+
const path = join(dir, entry.name);
|
|
98
|
+
if (entry.isDirectory())
|
|
99
|
+
visit(path);
|
|
100
|
+
else if (entry.isFile() && entry.name.endsWith(".spec.md"))
|
|
101
|
+
paths.push(path);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
visit(root);
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
// v1.89 T6: mechanically bind the complete spec-corpus denominator to the runner's all-project JSON
|
|
108
|
+
// listing. Every discovered path contributes either all parser-produced acceptance items or one named
|
|
109
|
+
// parse failure; exceptions are evidence, never permission to shrink the corpus silently.
|
|
110
|
+
export function auditAcceptanceCorpus(corpusRoot, listedTests) {
|
|
111
|
+
const runnerNames = listedTests.map((listed) => ({
|
|
112
|
+
listed,
|
|
113
|
+
fullName: listed.name.split(" > ").join(" "),
|
|
114
|
+
}));
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const specPath of corpusSpecPaths(corpusRoot)) {
|
|
117
|
+
try {
|
|
118
|
+
const graph = compileNative(specPath);
|
|
119
|
+
for (const task of graph.tasks) {
|
|
120
|
+
for (const item of task.acceptance) {
|
|
121
|
+
results.push({
|
|
122
|
+
specPath,
|
|
123
|
+
status: "parsed",
|
|
124
|
+
item,
|
|
125
|
+
...(typeof item === "object" && item.oracle === "test"
|
|
126
|
+
? {
|
|
127
|
+
namedTest: {
|
|
128
|
+
criterion: item.test,
|
|
129
|
+
matches: runnerNames
|
|
130
|
+
.filter(({ fullName }) => fullName === item.test)
|
|
131
|
+
.map(({ listed }) => listed),
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
results.push({ specPath, status: "parse-failed", error: `${specPath}: ${message}` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
83
146
|
// vitest/jest summary: "Tests N passed | M skipped (T)" — count passed+failed as actually ran.
|
|
84
147
|
function testsRan(output) {
|
|
85
148
|
const lines = output.replace(/\x1b\[[\d;#]*[A-Za-z]/g, "").split("\n");
|
|
@@ -185,23 +248,6 @@ function deletedPath(section) {
|
|
|
185
248
|
const unquoted = oldPath.startsWith('"') && oldPath.endsWith('"') ? oldPath.slice(1, -1) : oldPath;
|
|
186
249
|
return unquoted.replace(/^a\//, "");
|
|
187
250
|
}
|
|
188
|
-
// OBS-134: whole-file deletions are already fully described by their path. Sending every removed line
|
|
189
|
-
// spends the cap and judge context on content that cannot exist after the change. Added and modified
|
|
190
|
-
// sections pass through byte-for-byte, so their anti-flooding budget is unchanged.
|
|
191
|
-
// v1.82 T1 clause 5: two exemptions, because this filter triggers on the very `deleted file mode` line
|
|
192
|
-
// the set-aside preserves. Protected evidence (the frozen anchors, the captured journals) bypasses the
|
|
193
|
-
// collapse so its deletion half reaches the measured text and the judge complete; and a section already
|
|
194
|
-
// set aside is never reduced a second time, or this filter would erase the receipt that replaced it.
|
|
195
|
-
function judgeRelevantDiff(diff) {
|
|
196
|
-
return diff.split(DIFF_SECTIONS).map((section) => {
|
|
197
|
-
if (setAsideReceiptPath(section))
|
|
198
|
-
return section;
|
|
199
|
-
const path = deletedPath(section);
|
|
200
|
-
if (!path || isProtectedEvidence(path))
|
|
201
|
-
return section;
|
|
202
|
-
return `deleted file: ${path}\n`;
|
|
203
|
-
}).join("");
|
|
204
|
-
}
|
|
205
251
|
// v1.82 T1 clause 7: the two shapes this task creates carry no changed hunk, so a judge asked to cite a
|
|
206
252
|
// changed line has nothing to cite — three review rounds died here. Each yields ONE citable operation
|
|
207
253
|
// fact, `{path, line: 0}`, read back from the judged text itself. Only these RECORDED facts make line 0
|
|
@@ -272,10 +318,9 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
|
|
|
272
318
|
? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
|
|
273
319
|
: "";
|
|
274
320
|
const fetched = await fetchTaskDiff(worktree, baseRef);
|
|
275
|
-
const diff =
|
|
276
|
-
const forCap = judgeRelevantDiff(fetched.forCap);
|
|
321
|
+
const diff = reviewableLogicDiff(fetched.full);
|
|
277
322
|
const diffCap = opts.diffCap ?? DEFAULT_DIFF_CAP;
|
|
278
|
-
const capFail =
|
|
323
|
+
const capFail = checkTaskDiffCaps("acceptance", fetched, diffCap, warn + detBlock);
|
|
279
324
|
if (capFail)
|
|
280
325
|
return capFail;
|
|
281
326
|
const expectedIds = judgeItems.map((_, index) => judgeCriterionId(index));
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated artifacts are not inferred from their directory or extension. A
|
|
3
|
+
* path receives capture accounting only when its manifest row names a
|
|
4
|
+
* registered producer and repeats that producer's current provenance exactly.
|
|
5
|
+
*/
|
|
6
|
+
export type CaptureProducerProvenance = {
|
|
7
|
+
readonly source: string;
|
|
8
|
+
readonly entrypoint: string;
|
|
9
|
+
readonly revision: string;
|
|
10
|
+
};
|
|
11
|
+
export type CaptureProducerRegistration = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly provenance: CaptureProducerProvenance;
|
|
14
|
+
};
|
|
15
|
+
export type CaptureArtifactManifestEntry = {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly producer: string;
|
|
18
|
+
readonly provenance: CaptureProducerProvenance;
|
|
19
|
+
};
|
|
20
|
+
export type CaptureArtifactManifest = {
|
|
21
|
+
readonly version: 1;
|
|
22
|
+
readonly producers: readonly CaptureProducerRegistration[];
|
|
23
|
+
readonly artifacts: readonly CaptureArtifactManifestEntry[];
|
|
24
|
+
};
|
|
25
|
+
export declare const CAPTURE_PRODUCERS: readonly [{
|
|
26
|
+
readonly id: "cockpit-golden-frames";
|
|
27
|
+
readonly provenance: {
|
|
28
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
29
|
+
readonly entrypoint: "regenerateGoldenFrames";
|
|
30
|
+
readonly revision: "golden-frame-v1";
|
|
31
|
+
};
|
|
32
|
+
}, {
|
|
33
|
+
readonly id: "cockpit-colour-frames";
|
|
34
|
+
readonly provenance: {
|
|
35
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
36
|
+
readonly entrypoint: "regenerateColourFrames";
|
|
37
|
+
readonly revision: "colour-frame-v1";
|
|
38
|
+
};
|
|
39
|
+
}];
|
|
40
|
+
export declare const CAPTURE_ARTIFACT_MANIFEST: {
|
|
41
|
+
readonly version: 1;
|
|
42
|
+
readonly producers: readonly [{
|
|
43
|
+
readonly id: "cockpit-golden-frames";
|
|
44
|
+
readonly provenance: {
|
|
45
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
46
|
+
readonly entrypoint: "regenerateGoldenFrames";
|
|
47
|
+
readonly revision: "golden-frame-v1";
|
|
48
|
+
};
|
|
49
|
+
}, {
|
|
50
|
+
readonly id: "cockpit-colour-frames";
|
|
51
|
+
readonly provenance: {
|
|
52
|
+
readonly source: "src/tui/cockpit/capture.ts";
|
|
53
|
+
readonly entrypoint: "regenerateColourFrames";
|
|
54
|
+
readonly revision: "colour-frame-v1";
|
|
55
|
+
};
|
|
56
|
+
}];
|
|
57
|
+
readonly artifacts: readonly ({
|
|
58
|
+
path: string;
|
|
59
|
+
producer: string;
|
|
60
|
+
provenance: CaptureProducerProvenance;
|
|
61
|
+
} | {
|
|
62
|
+
path: string;
|
|
63
|
+
producer: string;
|
|
64
|
+
provenance: CaptureProducerProvenance;
|
|
65
|
+
})[];
|
|
66
|
+
};
|
|
67
|
+
/** Compatibility name for the pre-manifest gate API; now derived from one manifest. */
|
|
68
|
+
export declare const REGENERABLE_CAPTURE_PATHS: readonly string[];
|
|
69
|
+
export declare const PROTECTED_EVIDENCE_PREFIXES: readonly ["tests/fixtures/cockpit/anchors/", "tests/fixtures/cockpit/sources/", "tests/fixtures/cockpit/colour/sources/"];
|
|
70
|
+
export declare function isProtectedEvidence(path: string): boolean;
|
|
71
|
+
export type ArtifactPathClassification = {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly kind: "logic";
|
|
74
|
+
readonly reason: "protected-evidence" | "unmanifested" | "malformed-manifest" | "missing-producer" | "stale-provenance";
|
|
75
|
+
readonly error?: string;
|
|
76
|
+
} | {
|
|
77
|
+
readonly path: string;
|
|
78
|
+
readonly kind: "capture";
|
|
79
|
+
readonly reason: "manifest-provenance";
|
|
80
|
+
readonly producer: string;
|
|
81
|
+
readonly provenance: CaptureProducerProvenance;
|
|
82
|
+
};
|
|
83
|
+
export declare function classifyArtifactPath(path: string, manifest?: unknown): ArtifactPathClassification;
|
|
84
|
+
/** The path named by a citable capture receipt, or null when there is none. */
|
|
85
|
+
export declare function setAsideReceiptPath(section: string): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* Whole-file source deletions retain their operation fact instead of spending
|
|
88
|
+
* the reader cap on bytes that no longer exist. Capture receipts and protected
|
|
89
|
+
* evidence are deliberately exempt from this older reduction.
|
|
90
|
+
*/
|
|
91
|
+
export declare function reviewableLogicDiff(diff: string): string;
|
|
92
|
+
export type ArtifactDiffSection = {
|
|
93
|
+
readonly paths: readonly string[];
|
|
94
|
+
readonly kind: "capture" | "logic";
|
|
95
|
+
readonly reason: string;
|
|
96
|
+
readonly producer?: string;
|
|
97
|
+
readonly provenance?: CaptureProducerProvenance;
|
|
98
|
+
/** UTF-8 bytes left for a reviewer to read, including any receipt. */
|
|
99
|
+
readonly logicBytes: number;
|
|
100
|
+
/** UTF-8 bytes withheld behind the finite capture bound. */
|
|
101
|
+
readonly captureBytes: number;
|
|
102
|
+
};
|
|
103
|
+
export type ArtifactDiffMeasurement = {
|
|
104
|
+
readonly rendered: string;
|
|
105
|
+
readonly logicBytes: number;
|
|
106
|
+
readonly captureBytes: number;
|
|
107
|
+
readonly sections: readonly ArtifactDiffSection[];
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Classify and compact a Git diff once. Invalid manifest facts remain ordinary
|
|
111
|
+
* logic. Valid capture content is replaced by one citable receipt, while its
|
|
112
|
+
* exact UTF-8 payload remains charged to the separate capture bucket.
|
|
113
|
+
*/
|
|
114
|
+
export declare function measureArtifactDiff(diff: string, manifest?: unknown): ArtifactDiffMeasurement;
|
|
115
|
+
/** The original API now delegates to the provenance-backed measurement. */
|
|
116
|
+
export declare function setAsideRegenerableCaptures(diff: string): string;
|
|
117
|
+
export declare const MIN_CAPTURE_DIFF_CAP = 1000000;
|
|
118
|
+
export declare const CAPTURE_DIFF_CAP_MULTIPLIER = 4;
|
|
119
|
+
export declare function captureDiffCapFor(logicCap: number): number;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated artifacts are not inferred from their directory or extension. A
|
|
3
|
+
* path receives capture accounting only when its manifest row names a
|
|
4
|
+
* registered producer and repeats that producer's current provenance exactly.
|
|
5
|
+
*/
|
|
6
|
+
const GOLDEN_PROVENANCE = {
|
|
7
|
+
source: "src/tui/cockpit/capture.ts",
|
|
8
|
+
entrypoint: "regenerateGoldenFrames",
|
|
9
|
+
revision: "golden-frame-v1",
|
|
10
|
+
};
|
|
11
|
+
const COLOUR_PROVENANCE = {
|
|
12
|
+
source: "src/tui/cockpit/capture.ts",
|
|
13
|
+
entrypoint: "regenerateColourFrames",
|
|
14
|
+
revision: "colour-frame-v1",
|
|
15
|
+
};
|
|
16
|
+
export const CAPTURE_PRODUCERS = [
|
|
17
|
+
{ id: "cockpit-golden-frames", provenance: GOLDEN_PROVENANCE },
|
|
18
|
+
{ id: "cockpit-colour-frames", provenance: COLOUR_PROVENANCE },
|
|
19
|
+
];
|
|
20
|
+
const goldenFrameNames = [
|
|
21
|
+
"run.width-stacked.80x24.txt",
|
|
22
|
+
"run.width-folded-keys.100x24.txt",
|
|
23
|
+
"run.width-three-column.140x24.txt",
|
|
24
|
+
"run.height-14.140x14.txt",
|
|
25
|
+
"run.height-18.140x18.txt",
|
|
26
|
+
"run.height-24.140x24.txt",
|
|
27
|
+
"run.height-40.140x40.txt",
|
|
28
|
+
"run.no-colour.140x24.txt",
|
|
29
|
+
"run.non-tty.140x24.txt",
|
|
30
|
+
"run.ci.140x24.txt",
|
|
31
|
+
"setup.width-stacked.80x24.txt",
|
|
32
|
+
"setup.width-folded-keys.100x24.txt",
|
|
33
|
+
"setup.width-three-column.140x24.txt",
|
|
34
|
+
"setup.height-14.140x14.txt",
|
|
35
|
+
"setup.height-18.140x18.txt",
|
|
36
|
+
"setup.height-24.140x24.txt",
|
|
37
|
+
"setup.height-40.140x40.txt",
|
|
38
|
+
"setup.no-colour.140x24.txt",
|
|
39
|
+
"setup.non-tty.140x24.txt",
|
|
40
|
+
"setup.ci.140x24.txt",
|
|
41
|
+
];
|
|
42
|
+
const colourFrameNames = [
|
|
43
|
+
"run-20260718-000943.colour.140x24.txt",
|
|
44
|
+
"run-20260718-000943.no-colour.140x24.txt",
|
|
45
|
+
"run-20260725-025004.interrupted.colour.140x24.txt",
|
|
46
|
+
];
|
|
47
|
+
const provenanceCopy = (provenance) => ({ ...provenance });
|
|
48
|
+
export const CAPTURE_ARTIFACT_MANIFEST = {
|
|
49
|
+
version: 1,
|
|
50
|
+
producers: CAPTURE_PRODUCERS,
|
|
51
|
+
artifacts: [
|
|
52
|
+
...goldenFrameNames.map((fixture) => ({
|
|
53
|
+
path: `tests/fixtures/cockpit/frames/${fixture}`,
|
|
54
|
+
producer: "cockpit-golden-frames",
|
|
55
|
+
provenance: provenanceCopy(GOLDEN_PROVENANCE),
|
|
56
|
+
})),
|
|
57
|
+
...colourFrameNames.map((fixture) => ({
|
|
58
|
+
path: `tests/fixtures/cockpit/colour/${fixture}`,
|
|
59
|
+
producer: "cockpit-colour-frames",
|
|
60
|
+
provenance: provenanceCopy(COLOUR_PROVENANCE),
|
|
61
|
+
})),
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
/** Compatibility name for the pre-manifest gate API; now derived from one manifest. */
|
|
65
|
+
export const REGENERABLE_CAPTURE_PATHS = CAPTURE_ARTIFACT_MANIFEST.artifacts
|
|
66
|
+
.map((artifact) => artifact.path);
|
|
67
|
+
export const PROTECTED_EVIDENCE_PREFIXES = [
|
|
68
|
+
"tests/fixtures/cockpit/anchors/",
|
|
69
|
+
"tests/fixtures/cockpit/sources/",
|
|
70
|
+
"tests/fixtures/cockpit/colour/sources/",
|
|
71
|
+
];
|
|
72
|
+
export function isProtectedEvidence(path) {
|
|
73
|
+
return PROTECTED_EVIDENCE_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
74
|
+
}
|
|
75
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
76
|
+
function isCanonicalRepoPath(value) {
|
|
77
|
+
if (typeof value !== "string" || value.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
if (value.startsWith("/") || value.includes("\\"))
|
|
80
|
+
return false;
|
|
81
|
+
const parts = value.split("/");
|
|
82
|
+
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
83
|
+
}
|
|
84
|
+
function parseProvenance(value) {
|
|
85
|
+
if (!isRecord(value))
|
|
86
|
+
return null;
|
|
87
|
+
const { source, entrypoint, revision } = value;
|
|
88
|
+
if (!isCanonicalRepoPath(source))
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof entrypoint !== "string" || !/^[A-Za-z_$][\w$]*$/.test(entrypoint))
|
|
91
|
+
return null;
|
|
92
|
+
if (typeof revision !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(revision))
|
|
93
|
+
return null;
|
|
94
|
+
return { source, entrypoint, revision };
|
|
95
|
+
}
|
|
96
|
+
const sameProvenance = (left, right) => left.source === right.source
|
|
97
|
+
&& left.entrypoint === right.entrypoint
|
|
98
|
+
&& left.revision === right.revision;
|
|
99
|
+
function indexManifest(manifest) {
|
|
100
|
+
if (!isRecord(manifest) || manifest.version !== 1) {
|
|
101
|
+
return "artifact manifest version must be 1";
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(manifest.producers) || !Array.isArray(manifest.artifacts)) {
|
|
104
|
+
return "artifact manifest producers and artifacts must be arrays";
|
|
105
|
+
}
|
|
106
|
+
const producers = new Map();
|
|
107
|
+
for (const [index, raw] of manifest.producers.entries()) {
|
|
108
|
+
if (!isRecord(raw) || typeof raw.id !== "string" || !raw.id.trim()) {
|
|
109
|
+
return `artifact manifest producer ${index} is malformed`;
|
|
110
|
+
}
|
|
111
|
+
const provenance = parseProvenance(raw.provenance);
|
|
112
|
+
if (!provenance)
|
|
113
|
+
return `artifact manifest producer ${raw.id} has malformed provenance`;
|
|
114
|
+
if (producers.has(raw.id))
|
|
115
|
+
return `artifact manifest producer ${raw.id} is duplicated`;
|
|
116
|
+
producers.set(raw.id, { id: raw.id, provenance });
|
|
117
|
+
}
|
|
118
|
+
const artifacts = new Map();
|
|
119
|
+
for (const [index, raw] of manifest.artifacts.entries()) {
|
|
120
|
+
if (!isRecord(raw) || !isCanonicalRepoPath(raw.path)
|
|
121
|
+
|| typeof raw.producer !== "string" || !raw.producer.trim()) {
|
|
122
|
+
return `artifact manifest entry ${index} is malformed`;
|
|
123
|
+
}
|
|
124
|
+
const provenance = parseProvenance(raw.provenance);
|
|
125
|
+
if (!provenance)
|
|
126
|
+
return `artifact manifest entry ${raw.path} has malformed provenance`;
|
|
127
|
+
if (artifacts.has(raw.path))
|
|
128
|
+
return `artifact manifest path ${raw.path} is duplicated`;
|
|
129
|
+
artifacts.set(raw.path, { path: raw.path, producer: raw.producer, provenance });
|
|
130
|
+
}
|
|
131
|
+
return { producers, artifacts };
|
|
132
|
+
}
|
|
133
|
+
export function classifyArtifactPath(path, manifest = CAPTURE_ARTIFACT_MANIFEST) {
|
|
134
|
+
// Protected evidence wins even over a forged manifest row.
|
|
135
|
+
if (isProtectedEvidence(path)) {
|
|
136
|
+
return { path, kind: "logic", reason: "protected-evidence" };
|
|
137
|
+
}
|
|
138
|
+
const indexed = indexManifest(manifest);
|
|
139
|
+
if (typeof indexed === "string") {
|
|
140
|
+
return { path, kind: "logic", reason: "malformed-manifest", error: indexed };
|
|
141
|
+
}
|
|
142
|
+
const artifact = indexed.artifacts.get(path);
|
|
143
|
+
if (!artifact)
|
|
144
|
+
return { path, kind: "logic", reason: "unmanifested" };
|
|
145
|
+
const producer = indexed.producers.get(artifact.producer);
|
|
146
|
+
if (!producer) {
|
|
147
|
+
return {
|
|
148
|
+
path,
|
|
149
|
+
kind: "logic",
|
|
150
|
+
reason: "missing-producer",
|
|
151
|
+
error: `capture producer ${artifact.producer} is not registered`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!sameProvenance(artifact.provenance, producer.provenance)) {
|
|
155
|
+
return {
|
|
156
|
+
path,
|
|
157
|
+
kind: "logic",
|
|
158
|
+
reason: "stale-provenance",
|
|
159
|
+
error: `capture provenance for ${path} does not match producer ${producer.id}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
path,
|
|
164
|
+
kind: "capture",
|
|
165
|
+
reason: "manifest-provenance",
|
|
166
|
+
producer: producer.id,
|
|
167
|
+
provenance: producer.provenance,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const SET_ASIDE_RECEIPT = /^set aside: regenerable capture (.+?) — \d+ bytes withheld\b/m;
|
|
171
|
+
/** The path named by a citable capture receipt, or null when there is none. */
|
|
172
|
+
export function setAsideReceiptPath(section) {
|
|
173
|
+
return SET_ASIDE_RECEIPT.exec(section)?.[1] ?? null;
|
|
174
|
+
}
|
|
175
|
+
const DIFF_SECTIONS = /(?=^diff --git )/m;
|
|
176
|
+
function deletedPath(section) {
|
|
177
|
+
if (!/^deleted file mode /m.test(section))
|
|
178
|
+
return null;
|
|
179
|
+
const oldPath = /^--- (.+)$/m.exec(section)?.[1]
|
|
180
|
+
?? /^Binary files (.+) and \/dev\/null differ$/m.exec(section)?.[1];
|
|
181
|
+
if (!oldPath || oldPath === "/dev/null")
|
|
182
|
+
return null;
|
|
183
|
+
return unquoteGitPath(oldPath).replace(/^a\//, "");
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whole-file source deletions retain their operation fact instead of spending
|
|
187
|
+
* the reader cap on bytes that no longer exist. Capture receipts and protected
|
|
188
|
+
* evidence are deliberately exempt from this older reduction.
|
|
189
|
+
*/
|
|
190
|
+
export function reviewableLogicDiff(diff) {
|
|
191
|
+
return diff.split(DIFF_SECTIONS).map((section) => {
|
|
192
|
+
if (setAsideReceiptPath(section))
|
|
193
|
+
return section;
|
|
194
|
+
const path = deletedPath(section);
|
|
195
|
+
if (!path || isProtectedEvidence(path))
|
|
196
|
+
return section;
|
|
197
|
+
return `deleted file: ${path}\n`;
|
|
198
|
+
}).join("");
|
|
199
|
+
}
|
|
200
|
+
function unquoteGitPath(raw) {
|
|
201
|
+
const value = raw.trim();
|
|
202
|
+
if (!value.startsWith('"') || !value.endsWith('"'))
|
|
203
|
+
return value;
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(value);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return value.slice(1, -1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function diffSidePath(raw) {
|
|
212
|
+
const value = unquoteGitPath(raw);
|
|
213
|
+
if (value === "/dev/null")
|
|
214
|
+
return null;
|
|
215
|
+
return value.replace(/^[ab]\//, "");
|
|
216
|
+
}
|
|
217
|
+
function gitHeaderTokens(raw) {
|
|
218
|
+
return raw.match(/"(?:\\.|[^"\\])*"|\S+/g) ?? [];
|
|
219
|
+
}
|
|
220
|
+
function parseContentSection(section) {
|
|
221
|
+
const lines = section.split("\n");
|
|
222
|
+
const minus = lines.findIndex((line) => line.startsWith("--- "));
|
|
223
|
+
if (minus === -1 || !lines[minus + 1]?.startsWith("+++ "))
|
|
224
|
+
return null;
|
|
225
|
+
const body = lines.slice(minus + 2);
|
|
226
|
+
if (!body.some((line) => line.startsWith("@@ ")))
|
|
227
|
+
return null;
|
|
228
|
+
return {
|
|
229
|
+
lines,
|
|
230
|
+
minus,
|
|
231
|
+
sides: [
|
|
232
|
+
diffSidePath(lines[minus].slice(4)),
|
|
233
|
+
diffSidePath(lines[minus + 1].slice(4)),
|
|
234
|
+
],
|
|
235
|
+
body,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function sectionPaths(section, parsed) {
|
|
239
|
+
if (parsed)
|
|
240
|
+
return [...new Set(parsed.sides.filter((path) => path !== null))];
|
|
241
|
+
const renamedFrom = /^rename from (.+)$/m.exec(section)?.[1];
|
|
242
|
+
const renamedTo = /^rename to (.+)$/m.exec(section)?.[1];
|
|
243
|
+
if (renamedFrom || renamedTo) {
|
|
244
|
+
return [...new Set([renamedFrom, renamedTo].filter((path) => path !== undefined).map(unquoteGitPath))];
|
|
245
|
+
}
|
|
246
|
+
const header = /^diff --git (.+)$/m.exec(section)?.[1];
|
|
247
|
+
if (!header)
|
|
248
|
+
return [];
|
|
249
|
+
const tokens = gitHeaderTokens(header);
|
|
250
|
+
if (tokens.length !== 2)
|
|
251
|
+
return [];
|
|
252
|
+
return [...new Set(tokens.map(diffSidePath).filter((path) => path !== null))];
|
|
253
|
+
}
|
|
254
|
+
function hunkPayload(body, sign) {
|
|
255
|
+
return body
|
|
256
|
+
.filter((line) => line.startsWith(sign) || line.startsWith("\\"))
|
|
257
|
+
.map((line) => line[0] === sign ? line.slice(1) : line)
|
|
258
|
+
.join("\n");
|
|
259
|
+
}
|
|
260
|
+
function kindOnlyPaths(sections) {
|
|
261
|
+
const removed = new Map();
|
|
262
|
+
const added = new Map();
|
|
263
|
+
for (const section of sections) {
|
|
264
|
+
const parsed = parseContentSection(section);
|
|
265
|
+
if (!parsed)
|
|
266
|
+
continue;
|
|
267
|
+
const [before, after] = parsed.sides;
|
|
268
|
+
if (before && !after)
|
|
269
|
+
removed.set(before, hunkPayload(parsed.body, "-"));
|
|
270
|
+
else if (after && !before)
|
|
271
|
+
added.set(after, hunkPayload(parsed.body, "+"));
|
|
272
|
+
}
|
|
273
|
+
return new Set([...removed]
|
|
274
|
+
.filter(([path, payload]) => added.get(path) === payload)
|
|
275
|
+
.map(([path]) => path));
|
|
276
|
+
}
|
|
277
|
+
function sameCaptureProducer(classifications) {
|
|
278
|
+
if (!classifications.length || classifications.some((row) => row.kind !== "capture"))
|
|
279
|
+
return false;
|
|
280
|
+
const [first] = classifications;
|
|
281
|
+
return first !== undefined && classifications.every((row) => row.kind === "capture"
|
|
282
|
+
&& row.producer === first.producer
|
|
283
|
+
&& sameProvenance(row.provenance, first.provenance));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Classify and compact a Git diff once. Invalid manifest facts remain ordinary
|
|
287
|
+
* logic. Valid capture content is replaced by one citable receipt, while its
|
|
288
|
+
* exact UTF-8 payload remains charged to the separate capture bucket.
|
|
289
|
+
*/
|
|
290
|
+
export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST) {
|
|
291
|
+
if (!diff.includes("diff --git ")) {
|
|
292
|
+
return {
|
|
293
|
+
rendered: diff,
|
|
294
|
+
logicBytes: Buffer.byteLength(diff, "utf8"),
|
|
295
|
+
captureBytes: 0,
|
|
296
|
+
sections: [],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const rawSections = diff.split(/(?=^diff --git )/m);
|
|
300
|
+
const kindOnly = kindOnlyPaths(rawSections);
|
|
301
|
+
const rendered = [];
|
|
302
|
+
const measurements = [];
|
|
303
|
+
for (const section of rawSections) {
|
|
304
|
+
const parsed = parseContentSection(section);
|
|
305
|
+
const paths = sectionPaths(section, parsed);
|
|
306
|
+
const classifications = paths.map((path) => classifyArtifactPath(path, manifest));
|
|
307
|
+
const validCapture = sameCaptureProducer(classifications);
|
|
308
|
+
const producer = validCapture ? classifications[0] : undefined;
|
|
309
|
+
const isKindOnly = paths.some((path) => kindOnly.has(path));
|
|
310
|
+
if (!parsed || !validCapture || !producer || isKindOnly) {
|
|
311
|
+
rendered.push(section);
|
|
312
|
+
measurements.push({
|
|
313
|
+
paths,
|
|
314
|
+
kind: validCapture ? "capture" : "logic",
|
|
315
|
+
reason: validCapture
|
|
316
|
+
? parsed ? "content-identical-kind-change" : "file-operation-only"
|
|
317
|
+
: classifications.map((row) => row.reason).join(",") || "unparsed-diff-section",
|
|
318
|
+
...(producer ? { producer: producer.producer, provenance: producer.provenance } : {}),
|
|
319
|
+
logicBytes: Buffer.byteLength(section, "utf8"),
|
|
320
|
+
captureBytes: 0,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const withheld = parsed.lines.slice(parsed.minus).join("\n");
|
|
325
|
+
const captureBytes = Buffer.byteLength(withheld, "utf8");
|
|
326
|
+
const receiptPath = paths.at(-1);
|
|
327
|
+
const receipt = `set aside: regenerable capture ${receiptPath} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
328
|
+
const compact = `${parsed.lines.slice(0, parsed.minus).join("\n")}\n${receipt}\n`;
|
|
329
|
+
rendered.push(compact);
|
|
330
|
+
measurements.push({
|
|
331
|
+
paths,
|
|
332
|
+
kind: "capture",
|
|
333
|
+
reason: "manifest-provenance",
|
|
334
|
+
producer: producer.producer,
|
|
335
|
+
provenance: producer.provenance,
|
|
336
|
+
logicBytes: Buffer.byteLength(compact, "utf8"),
|
|
337
|
+
captureBytes,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
rendered: rendered.join(""),
|
|
342
|
+
logicBytes: measurements.reduce((sum, row) => sum + row.logicBytes, 0),
|
|
343
|
+
captureBytes: measurements.reduce((sum, row) => sum + row.captureBytes, 0),
|
|
344
|
+
sections: measurements,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** The original API now delegates to the provenance-backed measurement. */
|
|
348
|
+
export function setAsideRegenerableCaptures(diff) {
|
|
349
|
+
return measureArtifactDiff(diff).rendered;
|
|
350
|
+
}
|
|
351
|
+
// The default corpus is about 134 KiB. Capture bytes are bounded, never free;
|
|
352
|
+
// the floor avoids making a deliberately lowered logic cap reintroduce Q14.
|
|
353
|
+
export const MIN_CAPTURE_DIFF_CAP = 1_000_000;
|
|
354
|
+
export const CAPTURE_DIFF_CAP_MULTIPLIER = 4;
|
|
355
|
+
export function captureDiffCapFor(logicCap) {
|
|
356
|
+
return Math.max(MIN_CAPTURE_DIFF_CAP, logicCap * CAPTURE_DIFF_CAP_MULTIPLIER);
|
|
357
|
+
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { AcceptanceItem } from "../graph/schema.js";
|
|
3
|
+
import { type ShResult } from "../run/git.js";
|
|
3
4
|
import type { GateResult } from "./types.js";
|
|
5
|
+
export interface BaselineCommand {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
fingerprints: string[];
|
|
8
|
+
missingCommand?: boolean;
|
|
9
|
+
/** What this command actually took at capture, on a pristine tree. Absent in pre-v1.90 baselines. */
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
/** The ceiling that measurement implies, persisted so every later battery uses the same number. */
|
|
12
|
+
ceilingMs?: number;
|
|
13
|
+
}
|
|
4
14
|
export interface Baseline {
|
|
5
|
-
commands: Record<string,
|
|
6
|
-
exitCode: number;
|
|
7
|
-
fingerprints: string[];
|
|
8
|
-
missingCommand?: boolean;
|
|
9
|
-
}>;
|
|
15
|
+
commands: Record<string, BaselineCommand>;
|
|
10
16
|
warnings?: BaselineWarning[];
|
|
11
17
|
}
|
|
12
18
|
export interface BaselineWarning {
|
|
@@ -14,9 +20,43 @@ export interface BaselineWarning {
|
|
|
14
20
|
commands: string[];
|
|
15
21
|
reason: string;
|
|
16
22
|
}
|
|
23
|
+
export type FailureClassification = "regression" | "infra";
|
|
24
|
+
/**
|
|
25
|
+
* What a nonzero runner exit is evidence OF. `undefined` when the output names neither — the
|
|
26
|
+
* unreadable-runner case the existing fail-closed path already owns.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifyFailureOutput(output: string): FailureClassification | undefined;
|
|
17
29
|
export declare const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
|
|
18
30
|
export declare function fingerprint(output: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Q121s: the battery's forgiveness math, exported so tip-verify applies the IDENTICAL rule.
|
|
33
|
+
* Returns the failure fingerprints of `raw` that are NOT in the baseline entry (fresh), and
|
|
34
|
+
* whether the output carried no recognizable failure shape at all.
|
|
35
|
+
*/
|
|
36
|
+
export declare function freshFailures(entry: BaselineCommand | undefined, raw: string): {
|
|
37
|
+
failing: string[];
|
|
38
|
+
unreadable: boolean;
|
|
39
|
+
};
|
|
19
40
|
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* The ceiling a battery runs under: whichever is larger of the shipped constant and the slack applied
|
|
43
|
+
* to what this command actually measured. A pre-v1.90 baseline (no measurement) gets exactly the
|
|
44
|
+
* shipped constant, so nothing regresses on an existing on-disk baseline.json — the same read-old
|
|
45
|
+
* compat rule the fingerprint renormalizer follows.
|
|
46
|
+
*/
|
|
47
|
+
export declare const effectiveCeilingMs: (entry?: Pick<BaselineCommand, "durationMs" | "ceilingMs">) => number;
|
|
48
|
+
/**
|
|
49
|
+
* The ONLY producer of a `ceiling-kill` gate result. It is gated on `timedOut`, which git.ts sets in
|
|
50
|
+
* exactly one place — inside the timer callback that issues the SIGKILL — so a process that exited on
|
|
51
|
+
* its own can never reach this text however it exited: a slow red suite, an unreadable runner, exit
|
|
52
|
+
* 137 from a kill somebody ELSE sent. Returning `undefined` rather than a result keeps that structural:
|
|
53
|
+
* every caller must go on to the ordinary verdict path, it cannot fall through into this one.
|
|
54
|
+
*
|
|
55
|
+
* A kill is not a verdict. The battery was still running when the machine took it away, so its exit
|
|
56
|
+
* code is evidence about the ceiling and nothing about the work — which is why this reports `infra`
|
|
57
|
+
* and never enters baseline forgiveness (there is no runner output to forgive).
|
|
58
|
+
*/
|
|
59
|
+
export declare function ceilingKillResult(gate: string, r: ShResult, ceilingMs: number): GateResult | undefined;
|
|
20
60
|
export declare function captureBaseline(cwd: string, commands: Record<string, string>): Promise<Baseline>;
|
|
21
61
|
export interface VacuousOracleWarning {
|
|
22
62
|
kind: "vacuous-oracle";
|