tickmarkr 1.68.0 → 1.69.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.
Files changed (36) hide show
  1. package/dist/adapters/kimi.d.ts +25 -1
  2. package/dist/adapters/kimi.js +80 -0
  3. package/dist/adapters/types.d.ts +6 -0
  4. package/dist/cli/commands/eval.d.ts +4 -0
  5. package/dist/cli/commands/eval.js +26 -0
  6. package/dist/cli/index.d.ts +1 -1
  7. package/dist/cli/index.js +3 -1
  8. package/dist/eval/canary.d.ts +46 -0
  9. package/dist/eval/canary.js +113 -0
  10. package/dist/eval/dispatch.d.ts +31 -0
  11. package/dist/eval/dispatch.js +207 -0
  12. package/dist/eval/fixtures.d.ts +22 -0
  13. package/dist/eval/fixtures.js +85 -0
  14. package/dist/eval/report.d.ts +35 -0
  15. package/dist/eval/report.js +82 -0
  16. package/dist/eval/selfcheck.d.ts +22 -0
  17. package/dist/eval/selfcheck.js +177 -0
  18. package/dist/run/daemon.js +125 -102
  19. package/dist/run/interactive-seed.d.ts +15 -0
  20. package/dist/run/interactive-seed.js +25 -0
  21. package/fixtures/eval/canary/solution/a.txt +1 -0
  22. package/fixtures/eval/canary/spec.md +8 -0
  23. package/fixtures/eval/canary/start/a.txt +1 -0
  24. package/fixtures/eval/sample/solution/a.txt +1 -0
  25. package/fixtures/eval/sample/spec.md +8 -0
  26. package/fixtures/eval/sample/start/a.txt +1 -0
  27. package/fixtures/gsd-sample/07-live-check/07-01-PLAN.md +42 -0
  28. package/fixtures/gsd-sample/07-live-check/07-02-PLAN.md +21 -0
  29. package/fixtures/gsd-sample/07-live-check/07-03-PLAN.md +18 -0
  30. package/fixtures/gsd-sample/07-live-check/07-03-SUMMARY.md +1 -0
  31. package/fixtures/missing-mandatory-gate.native.md +10 -0
  32. package/fixtures/sample-pin.prd.md +18 -0
  33. package/fixtures/sample.native.md +35 -0
  34. package/fixtures/sample.prd.md +22 -0
  35. package/fixtures/speckit-sample/tasks.md +20 -0
  36. package/package.json +3 -2
@@ -0,0 +1,85 @@
1
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { isAbsolute, join, relative } from "node:path";
4
+ import { shGitOk } from "../run/git.js";
5
+ export const FIXTURE_REQUIRED_PARTS = ["start", "solution"];
6
+ function isFixtureDir(dir) {
7
+ for (const part of FIXTURE_REQUIRED_PARTS) {
8
+ const p = join(dir, part);
9
+ if (!existsSync(p) || !lstatSync(p).isDirectory())
10
+ return false;
11
+ }
12
+ return true;
13
+ }
14
+ function missingPart(dir) {
15
+ for (const part of FIXTURE_REQUIRED_PARTS) {
16
+ const p = join(dir, part);
17
+ if (!existsSync(p) || !lstatSync(p).isDirectory())
18
+ return part;
19
+ }
20
+ return undefined;
21
+ }
22
+ function copyDirContents(src, dst) {
23
+ for (const ent of readdirSync(src, { withFileTypes: true })) {
24
+ const from = join(src, ent.name);
25
+ const to = join(dst, ent.name);
26
+ if (ent.isDirectory()) {
27
+ mkdirSync(to, { recursive: true });
28
+ copyDirContents(from, to);
29
+ }
30
+ else {
31
+ writeFileSync(to, readFileSync(from));
32
+ }
33
+ }
34
+ }
35
+ export function discoverFixtures(root) {
36
+ const valid = [];
37
+ const invalid = [];
38
+ if (!existsSync(root) || !lstatSync(root).isDirectory()) {
39
+ return { valid, invalid };
40
+ }
41
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
42
+ if (!ent.isDirectory())
43
+ continue;
44
+ const dir = join(root, ent.name);
45
+ if (isFixtureDir(dir)) {
46
+ valid.push({
47
+ id: relative(root, dir),
48
+ path: dir,
49
+ startDir: join(dir, "start"),
50
+ solutionDir: join(dir, "solution"),
51
+ });
52
+ }
53
+ else {
54
+ const part = missingPart(dir);
55
+ invalid.push({
56
+ id: relative(root, dir),
57
+ path: dir,
58
+ reason: part ? `missing required part: ${part}` : "missing required part",
59
+ });
60
+ }
61
+ }
62
+ valid.sort((a, b) => a.id.localeCompare(b.id));
63
+ invalid.sort((a, b) => a.id.localeCompare(b.id));
64
+ return { valid, invalid };
65
+ }
66
+ export async function seedFixture(fixture) {
67
+ const repo = mkdtempSync(join(tmpdir(), "tickmarkr-eval-"));
68
+ await shGitOk("git init -b main", repo);
69
+ await shGitOk("git config user.email tickmarkr@eval.local", repo);
70
+ await shGitOk("git config user.name tickmarkr-eval", repo);
71
+ // Copy only the starting files; the reference solution must never be seeded.
72
+ copyDirContents(fixture.startDir, repo);
73
+ await shGitOk("git add -A", repo);
74
+ await shGitOk("git commit -m init --no-gpg-sign", repo);
75
+ return {
76
+ repo,
77
+ cleanup: async () => {
78
+ rmSync(repo, { recursive: true, force: true });
79
+ },
80
+ };
81
+ }
82
+ export function resolveFixturesRoot(arg, cwd) {
83
+ const root = arg ?? "fixtures/eval";
84
+ return isAbsolute(root) ? root : join(cwd, root);
85
+ }
@@ -0,0 +1,35 @@
1
+ import type { BillingChannel, WorkerResult } from "../adapters/types.js";
2
+ import type { AcceptanceRunResult, ChannelResult } from "./dispatch.js";
3
+ import type { Fixture } from "./fixtures.js";
4
+ export interface ReportedResult {
5
+ runAt: string;
6
+ fixture: {
7
+ id: string;
8
+ revision: string;
9
+ };
10
+ channel: BillingChannel & {
11
+ channelKey: string;
12
+ };
13
+ skipped: boolean;
14
+ skipReason?: string;
15
+ worker?: WorkerResult;
16
+ acceptance?: AcceptanceRunResult;
17
+ }
18
+ /**
19
+ * Deterministic content identity for a fixture revision. Hashes every file under
20
+ * the fixture root (start + solution) so any change to the fixture's contents
21
+ * changes the revision digest.
22
+ */
23
+ export declare function fixtureRevisionHash(fixture: Fixture): string;
24
+ /**
25
+ * Append a single fixture-channel result to the incremental JSON output file.
26
+ * The write is synchronous so the bytes are on disk before the caller moves on
27
+ * to the next channel.
28
+ */
29
+ export declare function appendChannelResult(path: string, fixture: Fixture, result: ChannelResult, runAt?: string): void;
30
+ /**
31
+ * Read every parseable line from an incremental JSON report. Torn or corrupt
32
+ * trailing lines are ignored, so an interrupted run still yields every result
33
+ * that was fully written.
34
+ */
35
+ export declare function readReport(path: string): ReportedResult[];
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
3
+ import { dirname, relative } from "node:path";
4
+ const EXCLUDED_NAMES = new Set([".git", "node_modules", ".DS_Store"]);
5
+ /**
6
+ * Deterministic content identity for a fixture revision. Hashes every file under
7
+ * the fixture root (start + solution) so any change to the fixture's contents
8
+ * changes the revision digest.
9
+ */
10
+ export function fixtureRevisionHash(fixture) {
11
+ const hash = createHash("sha256");
12
+ function walk(dir) {
13
+ const entries = readdirSync(dir, { withFileTypes: true });
14
+ entries.sort((a, b) => a.name.localeCompare(b.name));
15
+ for (const ent of entries) {
16
+ if (EXCLUDED_NAMES.has(ent.name))
17
+ continue;
18
+ const full = `${dir}/${ent.name}`;
19
+ const rel = relative(fixture.path, full);
20
+ if (ent.isDirectory()) {
21
+ walk(full);
22
+ }
23
+ else if (ent.isFile()) {
24
+ hash.update(`${rel}\0`);
25
+ hash.update(readFileSync(full));
26
+ hash.update("\0");
27
+ }
28
+ }
29
+ }
30
+ if (existsSync(fixture.path)) {
31
+ walk(fixture.path);
32
+ }
33
+ return hash.digest("hex");
34
+ }
35
+ /**
36
+ * Append a single fixture-channel result to the incremental JSON output file.
37
+ * The write is synchronous so the bytes are on disk before the caller moves on
38
+ * to the next channel.
39
+ */
40
+ export function appendChannelResult(path, fixture, result, runAt) {
41
+ const dir = dirname(path);
42
+ if (!existsSync(dir)) {
43
+ mkdirSync(dir, { recursive: true });
44
+ }
45
+ const row = {
46
+ runAt: runAt ?? new Date().toISOString(),
47
+ fixture: {
48
+ id: fixture.id,
49
+ revision: fixtureRevisionHash(fixture),
50
+ },
51
+ channel: {
52
+ ...result.channel,
53
+ channelKey: result.channelKey,
54
+ },
55
+ skipped: result.skipped,
56
+ ...(result.skipReason !== undefined ? { skipReason: result.skipReason } : {}),
57
+ ...(result.worker !== undefined ? { worker: result.worker } : {}),
58
+ ...(result.acceptance !== undefined ? { acceptance: result.acceptance } : {}),
59
+ };
60
+ appendFileSync(path, JSON.stringify(row) + "\n");
61
+ }
62
+ /**
63
+ * Read every parseable line from an incremental JSON report. Torn or corrupt
64
+ * trailing lines are ignored, so an interrupted run still yields every result
65
+ * that was fully written.
66
+ */
67
+ export function readReport(path) {
68
+ if (!existsSync(path))
69
+ return [];
70
+ const out = [];
71
+ for (const line of readFileSync(path, "utf8").split("\n")) {
72
+ if (!line.trim())
73
+ continue;
74
+ try {
75
+ out.push(JSON.parse(line));
76
+ }
77
+ catch {
78
+ // torn trailing write after an interrupt — ignore; earlier lines are intact
79
+ }
80
+ }
81
+ return out;
82
+ }
@@ -0,0 +1,22 @@
1
+ import type { Fixture } from "./fixtures.js";
2
+ export interface SelfcheckRunResult {
3
+ pass: boolean;
4
+ details: string;
5
+ }
6
+ export interface SelfcheckResult {
7
+ fixtureId: string;
8
+ specPath: string;
9
+ start: SelfcheckRunResult;
10
+ solution: SelfcheckRunResult;
11
+ valid: boolean;
12
+ invalidReason?: string;
13
+ }
14
+ /**
15
+ * Run the fixture selfcheck: compile the fixture's own spec and run its deterministic acceptance
16
+ * oracles (command/test) against both the starting tree and the reference solution tree.
17
+ *
18
+ * The fixture is valid only when the starting tree fails the acceptance check and the reference
19
+ * solution tree passes the identical check — mechanizing the starting-state-must-fail principle
20
+ * already applied to shell-runnable oracles.
21
+ */
22
+ export declare function runSelfcheck(fixture: Fixture): Promise<SelfcheckResult>;
@@ -0,0 +1,177 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { DEFAULT_CONFIG } from "../config/config.js";
4
+ import { compileSource } from "../compile/index.js";
5
+ import { detectGateCommands } from "../gates/baseline.js";
6
+ import { testFiltered } from "../gates/acceptance.js";
7
+ import { renderAcceptanceItem } from "../graph/schema.js";
8
+ import { sh } from "../run/git.js";
9
+ function findSpec(fixtureDir) {
10
+ const direct = join(fixtureDir, "spec.md");
11
+ if (existsSync(direct))
12
+ return direct;
13
+ for (const ent of readdirSync(fixtureDir, { withFileTypes: true })) {
14
+ if (!ent.isFile())
15
+ continue;
16
+ const { name } = ent;
17
+ if (name.endsWith(".native.md") ||
18
+ name.endsWith(".prd.md") ||
19
+ name.endsWith(".spec.md") ||
20
+ name === "tasks.md") {
21
+ return join(fixtureDir, name);
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+ function isDeterministic(item) {
27
+ return typeof item === "object" && (item.oracle === "command" || item.oracle === "test");
28
+ }
29
+ // Mirrors the vitest/jest summary parser in src/gates/acceptance.ts so the selfcheck uses the same
30
+ // fail-closed rule for named-test oracles: exit 0 is vacuous if the name filter matched zero tests.
31
+ function testsRan(output) {
32
+ const lines = output.replace(/\x1b\[[\d;#]*[A-Za-z]/g, "").split("\n");
33
+ for (let i = lines.length - 1; i >= 0; i--) {
34
+ const line = lines[i].trim();
35
+ const m = line.match(/^Tests\s+(.+?)\s*\(\d+\)\s*$/);
36
+ if (!m)
37
+ continue;
38
+ let ran = 0;
39
+ for (const chunk of m[1].split("|").map((s) => s.trim())) {
40
+ const n = chunk.match(/^(\d+)\s+(passed|failed)\b/);
41
+ if (n)
42
+ ran += Number(n[1]);
43
+ }
44
+ return ran;
45
+ }
46
+ return null;
47
+ }
48
+ function tail(out, n = 8) {
49
+ const t = out.trim();
50
+ if (!t)
51
+ return "";
52
+ return "\n" + t.split("\n").slice(-n).join("\n");
53
+ }
54
+ async function runDeterministicAcceptance(task, cwd) {
55
+ const testCmd = detectGateCommands(cwd, DEFAULT_CONFIG).test;
56
+ const passed = [];
57
+ for (const item of task.acceptance) {
58
+ if (!isDeterministic(item))
59
+ continue;
60
+ if (item.oracle === "command") {
61
+ const r = await sh(item.command, cwd);
62
+ if (r.code !== 0) {
63
+ return {
64
+ pass: false,
65
+ details: `oracle failed: ${renderAcceptanceItem(item)} (exit ${r.code})${tail(r.stderr || r.stdout)}`,
66
+ };
67
+ }
68
+ passed.push(`✓ ${renderAcceptanceItem(item)} (exit 0)`);
69
+ }
70
+ else {
71
+ if (!testCmd) {
72
+ return {
73
+ pass: false,
74
+ details: `oracle failed: ${renderAcceptanceItem(item)} — no test command detected (failing closed)`,
75
+ };
76
+ }
77
+ const r = await sh(testFiltered(testCmd, item.test), cwd);
78
+ const out = (r.stderr || "") + "\n" + (r.stdout || "");
79
+ if (r.code !== 0) {
80
+ return {
81
+ pass: false,
82
+ details: `oracle failed: ${renderAcceptanceItem(item)} (exit ${r.code})${tail(r.stderr || r.stdout)}`,
83
+ };
84
+ }
85
+ const ran = testsRan(out);
86
+ if (ran === null || ran < 1) {
87
+ return {
88
+ pass: false,
89
+ details: `oracle failed: ${renderAcceptanceItem(item)} — name filter matched zero tests (filter: ${item.test})${tail(out)}`,
90
+ };
91
+ }
92
+ passed.push(`✓ ${renderAcceptanceItem(item)} (exit 0)`);
93
+ }
94
+ }
95
+ return {
96
+ pass: true,
97
+ details: passed.length ? passed.join("\n") : "no deterministic acceptance oracles",
98
+ };
99
+ }
100
+ /**
101
+ * Run the fixture selfcheck: compile the fixture's own spec and run its deterministic acceptance
102
+ * oracles (command/test) against both the starting tree and the reference solution tree.
103
+ *
104
+ * The fixture is valid only when the starting tree fails the acceptance check and the reference
105
+ * solution tree passes the identical check — mechanizing the starting-state-must-fail principle
106
+ * already applied to shell-runnable oracles.
107
+ */
108
+ export async function runSelfcheck(fixture) {
109
+ const specPath = findSpec(fixture.path);
110
+ if (!specPath) {
111
+ return {
112
+ fixtureId: fixture.id,
113
+ specPath: "",
114
+ start: { pass: false, details: "no spec found" },
115
+ solution: { pass: false, details: "no spec found" },
116
+ valid: false,
117
+ invalidReason: "fixture has no spec (expected spec.md or a recognized spec file)",
118
+ };
119
+ }
120
+ let task;
121
+ try {
122
+ const graph = compileSource(specPath);
123
+ if (graph.tasks.length !== 1) {
124
+ return {
125
+ fixtureId: fixture.id,
126
+ specPath,
127
+ start: { pass: false, details: "spec has more than one task" },
128
+ solution: { pass: false, details: "spec has more than one task" },
129
+ valid: false,
130
+ invalidReason: `fixture spec must contain exactly one task (found ${graph.tasks.length})`,
131
+ };
132
+ }
133
+ task = graph.tasks[0];
134
+ const runnable = task.acceptance.some(isDeterministic);
135
+ if (!runnable) {
136
+ return {
137
+ fixtureId: fixture.id,
138
+ specPath,
139
+ start: { pass: false, details: "no deterministic oracles" },
140
+ solution: { pass: false, details: "no deterministic oracles" },
141
+ valid: false,
142
+ invalidReason: "fixture has no shell-runnable acceptance oracle (command: or test:)",
143
+ };
144
+ }
145
+ }
146
+ catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ return {
149
+ fixtureId: fixture.id,
150
+ specPath,
151
+ start: { pass: false, details: message },
152
+ solution: { pass: false, details: message },
153
+ valid: false,
154
+ invalidReason: `fixture spec failed to compile: ${message}`,
155
+ };
156
+ }
157
+ const start = await runDeterministicAcceptance(task, fixture.startDir);
158
+ const solution = await runDeterministicAcceptance(task, fixture.solutionDir);
159
+ let valid = true;
160
+ let invalidReason;
161
+ if (start.pass) {
162
+ valid = false;
163
+ invalidReason = "starting tree already passes the acceptance check";
164
+ }
165
+ else if (!solution.pass) {
166
+ valid = false;
167
+ invalidReason = "reference tree still fails the acceptance check";
168
+ }
169
+ return {
170
+ fixtureId: fixture.id,
171
+ specPath,
172
+ start,
173
+ solution,
174
+ valid,
175
+ invalidReason,
176
+ };
177
+ }