do-better 1.0.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/LICENSE +21 -0
- package/README.md +265 -0
- package/bin/cli.js +262 -0
- package/do-better/SKILL.md +417 -0
- package/do-better/references/refute-charter.md +170 -0
- package/do-better/references/scoring.md +98 -0
- package/do-better/references/taxonomy.md +136 -0
- package/do-better/references/templates/charter-template.md +71 -0
- package/do-better/references/templates/finding-template.md +56 -0
- package/do-better/references/templates/rail-template.md +74 -0
- package/do-better/references/templates/roadmap-template.md +67 -0
- package/do-better/references/templates/ticket-template.md +78 -0
- package/do-better/references/verification.md +129 -0
- package/package.json +20 -0
- package/src/adlc.js +331 -0
- package/src/artifacts.js +580 -0
- package/src/charter.js +657 -0
- package/src/comprehend.js +553 -0
- package/src/identify.js +1119 -0
- package/src/llm.js +530 -0
- package/src/rail.js +533 -0
- package/src/refresh.js +256 -0
- package/src/roadmap.js +630 -0
- package/src/scan.js +313 -0
- package/src/state.js +260 -0
- package/src/utils.js +449 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Reproduce-or-Kill — D2 Verification Protocol (F4 defense)
|
|
2
|
+
|
|
3
|
+
Verification is separated from finding. The agent (or pass) that proposed a
|
|
4
|
+
candidate never judges it. Every candidate is either **reproduced** — and
|
|
5
|
+
becomes a verified finding with a reproduction record — or **killed** and
|
|
6
|
+
counted. There is no third bucket: *unverified findings never reach output.*
|
|
7
|
+
|
|
8
|
+
## Evidence-citation rules (apply everywhere, not just D2)
|
|
9
|
+
|
|
10
|
+
1. **Canonical citation format**, inline in every artifact body:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
path/to/file.js:123@a1b2c3d
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`path` is repo-relative, `line` is 1-based, `@sha` is the 7–40 hex commit
|
|
17
|
+
sha the claim was read at. A claim is pinned to the code as it was, so
|
|
18
|
+
`refresh` can detect when the ground moved under it.
|
|
19
|
+
|
|
20
|
+
2. **Deterministic verification, no LLM.** A citation verifies iff the file
|
|
21
|
+
exists in the worktree and `1 ≤ line ≤ line count`. This is a filesystem
|
|
22
|
+
check, never a model judgment.
|
|
23
|
+
|
|
24
|
+
3. **Every finding carries ≥ 1 verified citation.** A claim line whose
|
|
25
|
+
citations all fail verification is removed and logged. Behavior-inventory
|
|
26
|
+
entries with zero verified citations are dropped with a warning.
|
|
27
|
+
|
|
28
|
+
4. **Stale ≠ trusted.** When `refresh` finds a cited file changed since the
|
|
29
|
+
pinned sha, the claim is flagged stale, not silently believed (skill-rot
|
|
30
|
+
doctrine: a stale claim is misinformation with the voice of authority).
|
|
31
|
+
|
|
32
|
+
## The protocol, per candidate
|
|
33
|
+
|
|
34
|
+
### Step 1 — Citation check (deterministic)
|
|
35
|
+
|
|
36
|
+
Verify the candidate's citation per the rules above. Fails → **killed**. No
|
|
37
|
+
appeal: a finder that cited a nonexistent location fabricated evidence.
|
|
38
|
+
|
|
39
|
+
### Step 2 — Mechanical reproduction (preferred)
|
|
40
|
+
|
|
41
|
+
If the claim is mechanically checkable, ask the model to propose a `reproCmd`
|
|
42
|
+
from a **whitelist of shapes** — nothing outside it is ever executed:
|
|
43
|
+
|
|
44
|
+
- `node --test <file>` — run an existing test file.
|
|
45
|
+
- `node -e "<snippet>"` — a self-contained check, snippet ≤ 500 chars.
|
|
46
|
+
- A grep-equivalent pattern check, executed natively (no shell).
|
|
47
|
+
|
|
48
|
+
Execution discipline: `cwd` = repo root, no shell (`shell:false`), 30-second
|
|
49
|
+
timeout, stdout/stderr and exit code recorded verbatim. The command
|
|
50
|
+
demonstrating the claim succeeds → **verified**, `method: "command"`, the full
|
|
51
|
+
record stored in the finding.
|
|
52
|
+
|
|
53
|
+
**Only a _demonstrative_ check may alone verify (H9).** A check is demonstrative
|
|
54
|
+
when it genuinely ties to the claim: a repo-authored `node --test <file>`, or a
|
|
55
|
+
grep with a **specific** pattern. A model-proposed `node -e` snippet and a
|
|
56
|
+
catch-all grep (`.`, `.*`, empty, and the like — patterns that match virtually
|
|
57
|
+
any non-empty file) are **not** demonstrative: they can exit 0 without showing
|
|
58
|
+
anything, so a candidate backed only by one is not verified on the command — it
|
|
59
|
+
falls through to the blind re-read (Step 3), which must independently CONFIRM.
|
|
60
|
+
A non-demonstrative `node -e` is never executed at all, removing that
|
|
61
|
+
prompt-injection surface for hostile brownfield repos.
|
|
62
|
+
|
|
63
|
+
### Step 3 — Blind re-read (for non-runnable claims)
|
|
64
|
+
|
|
65
|
+
Design and debt claims can't run. For these, a **fresh frontier context** —
|
|
66
|
+
which has seen none of the finder's reasoning — receives only:
|
|
67
|
+
|
|
68
|
+
- the bare claim (title + one-paragraph claim text), and
|
|
69
|
+
- the cited code slice: the cited line ± 40 lines.
|
|
70
|
+
|
|
71
|
+
It re-derives a verdict from the code alone:
|
|
72
|
+
|
|
73
|
+
- **CONFIRM** — the code, read cold, supports the claim → verified,
|
|
74
|
+
`method: "reread"`, the verdict rationale recorded.
|
|
75
|
+
- **KILL** — the code does not support the claim → killed.
|
|
76
|
+
- **UNCERTAIN** — cannot confirm from the slice → **killed**. Uncertainty is
|
|
77
|
+
not a finding.
|
|
78
|
+
|
|
79
|
+
The blindness is load-bearing (F2): a verifier shown the proposer's reasoning
|
|
80
|
+
rubber-stamps it. Withhold everything but the claim and the code.
|
|
81
|
+
|
|
82
|
+
### Step 4 — Fail closed
|
|
83
|
+
|
|
84
|
+
- Unparseable verdict → killed.
|
|
85
|
+
- Repro command errors, times out, or exits unexpectedly without demonstrating
|
|
86
|
+
the claim → killed.
|
|
87
|
+
- Offline mode: only candidates whose reproduction is a deterministic native
|
|
88
|
+
check survive (`method: "static"`); everything requiring model judgment is
|
|
89
|
+
killed for the run and may be re-found online later.
|
|
90
|
+
|
|
91
|
+
Killed candidates are **counted** (per dimension, in state) but never written
|
|
92
|
+
as findings. The kill count is diagnostic, not shameful — a healthy finder
|
|
93
|
+
over-generates and a healthy verifier prunes hard.
|
|
94
|
+
|
|
95
|
+
## The reproduction record
|
|
96
|
+
|
|
97
|
+
Every verified finding stores, in frontmatter and body:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
reproduction:
|
|
101
|
+
method: command | reread | static
|
|
102
|
+
record: <the command + captured output, or the verdict rationale>
|
|
103
|
+
exitCode: <integer or null>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The record is what makes the finding **re-checkable**: `refresh` re-runs
|
|
107
|
+
`command`/`static` reproductions and re-verdicts `reread` ones against the
|
|
108
|
+
fresh slice. A finding that no longer reproduces is marked RESOLVED and its
|
|
109
|
+
roadmap item flips to done — the living-document loop closes only because
|
|
110
|
+
every finding ships with the means of its own re-verification.
|
|
111
|
+
|
|
112
|
+
## The gate
|
|
113
|
+
|
|
114
|
+
D2 passes its deterministic gate iff:
|
|
115
|
+
|
|
116
|
+
1. every `(dimension × packet)` cell went dry within `MAX_PASSES` (see
|
|
117
|
+
[refute-charter.md](./refute-charter.md)) — the deep-read set is partitioned
|
|
118
|
+
into packets and each cell loops-until-dry independently, so the gate detail
|
|
119
|
+
names the dimension *and* the packet that stalled, and
|
|
120
|
+
2. the count of unverified findings written is exactly **zero** — true by
|
|
121
|
+
construction, asserted anyway.
|
|
122
|
+
|
|
123
|
+
An empty or unreadable deep-read set online is also a gate failure (exit 2):
|
|
124
|
+
with no packets to examine, "dry" would be vacuously true, so starvation is
|
|
125
|
+
surfaced rather than passed silently. Offline is exempt — it consumes no
|
|
126
|
+
packets and runs a single deterministic static pass per dimension.
|
|
127
|
+
|
|
128
|
+
Either condition failing is exit 2. The findings count is never a success
|
|
129
|
+
metric (D11); ten verified findings beat a hundred plausible ones.
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "do-better",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Brownfield codebase analysis → verified findings → technical roadmap. The brownfield front-end to the ADLC.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "do-better": "bin/cli.js", "dobetter": "bin/cli.js" },
|
|
7
|
+
"files": ["bin/", "src/", "do-better/"],
|
|
8
|
+
"engines": { "node": ">=18.0.0" },
|
|
9
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test",
|
|
12
|
+
"prepublishOnly": "node --test"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["brownfield", "codebase-analysis", "roadmap", "characterization-tests", "adlc"],
|
|
15
|
+
"author": "Chris Williams (@voodootikigod)",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": { "type": "git", "url": "git+https://github.com/voodootikigod/do-better.git" },
|
|
18
|
+
"bugs": { "url": "https://github.com/voodootikigod/do-better/issues" },
|
|
19
|
+
"homepage": "https://github.com/voodootikigod/do-better#readme"
|
|
20
|
+
}
|
package/src/adlc.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/adlc.js — locate + spawn the five @adlc package CLIs and skill-mining
|
|
2
|
+
// (SPEC §5 composition contracts). The aidlc bins import core via relative
|
|
3
|
+
// paths, so they must run in place inside an aidlc checkout — never copied out.
|
|
4
|
+
//
|
|
5
|
+
// Absence is never an exception: every wrapper returns { skipped: true, reason }
|
|
6
|
+
// when its tool is unavailable, and the CALLER applies (and declares) the
|
|
7
|
+
// graceful degradation from the blueprint §5 table.
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { OpError, log, makeExec } from "./utils.js";
|
|
13
|
+
|
|
14
|
+
export const ADLC_TOOLS = ["parallax", "coldstart", "hollow-test", "behavior-diff", "preflight"];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 600000;
|
|
17
|
+
const REASON_NOT_INSTALLED = "not installed";
|
|
18
|
+
|
|
19
|
+
function defaultPackageRoot() {
|
|
20
|
+
// src/adlc.js → package root is one level up.
|
|
21
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isTruthyEnv(v) {
|
|
25
|
+
return v != null && v !== "" && v !== "0" && v !== "false";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function adlcBinPath(dir, name) {
|
|
29
|
+
return path.join(dir, "packages", name, "bin", `${name}.mjs`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function probeAdlcDir(dir) {
|
|
33
|
+
const available = {};
|
|
34
|
+
for (const name of ADLC_TOOLS) {
|
|
35
|
+
available[name] = existsSync(adlcBinPath(dir, name));
|
|
36
|
+
}
|
|
37
|
+
return available;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function emptyAvailability() {
|
|
41
|
+
const available = {};
|
|
42
|
+
for (const name of ADLC_TOOLS) available[name] = false;
|
|
43
|
+
available["skill-mining"] = false;
|
|
44
|
+
return available;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Locate an aidlc checkout (and a skill-mining checkout, separately).
|
|
48
|
+
// Probe order (first hit wins): env.DOBETTER_ADLC_DIR → sibling of the
|
|
49
|
+
// do-better package → sibling of the target repo → npx fallback. Setting
|
|
50
|
+
// DOBETTER_NO_NPX disables the npx fallback (mode "absent") so air-gapped runs
|
|
51
|
+
// and tests degrade deterministically instead of touching the network.
|
|
52
|
+
export function locateAdlc({ env = process.env, packageRoot, targetRoot } = {}) {
|
|
53
|
+
const pkgRoot = packageRoot ?? defaultPackageRoot();
|
|
54
|
+
const tgtRoot = targetRoot ?? process.cwd();
|
|
55
|
+
const noNpx = isTruthyEnv(env.DOBETTER_NO_NPX);
|
|
56
|
+
|
|
57
|
+
let mode = "absent";
|
|
58
|
+
let dir = null;
|
|
59
|
+
const available = emptyAvailability();
|
|
60
|
+
|
|
61
|
+
const candidates = [];
|
|
62
|
+
if (env.DOBETTER_ADLC_DIR) {
|
|
63
|
+
candidates.push({ dir: path.resolve(env.DOBETTER_ADLC_DIR), explicit: true });
|
|
64
|
+
}
|
|
65
|
+
candidates.push({ dir: path.resolve(pkgRoot, "..", "aidlc") });
|
|
66
|
+
candidates.push({ dir: path.resolve(tgtRoot, "..", "aidlc") });
|
|
67
|
+
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const probed = probeAdlcDir(candidate.dir);
|
|
70
|
+
if (ADLC_TOOLS.some((name) => probed[name])) {
|
|
71
|
+
mode = "dir";
|
|
72
|
+
dir = candidate.dir;
|
|
73
|
+
for (const name of ADLC_TOOLS) available[name] = probed[name];
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (candidate.explicit) {
|
|
77
|
+
log.warn(
|
|
78
|
+
`DOBETTER_ADLC_DIR=${candidate.dir} contains no @adlc tool bins ` +
|
|
79
|
+
`(expected packages/<name>/bin/<name>.mjs) — continuing probe.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (mode === "absent" && !noNpx) {
|
|
85
|
+
// npx fallback: each tool is verified lazily on first use; a failed npx
|
|
86
|
+
// resolve flips that tool to unavailable for the session.
|
|
87
|
+
mode = "npx";
|
|
88
|
+
for (const name of ADLC_TOOLS) available[name] = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// skill-mining is located separately (different repo layout: bin/cli.js).
|
|
92
|
+
let smMode = "absent";
|
|
93
|
+
let smDir = null;
|
|
94
|
+
const smCandidates = [];
|
|
95
|
+
if (env.DOBETTER_SKILL_MINING_DIR) {
|
|
96
|
+
smCandidates.push({ dir: path.resolve(env.DOBETTER_SKILL_MINING_DIR), explicit: true });
|
|
97
|
+
}
|
|
98
|
+
smCandidates.push({ dir: path.resolve(pkgRoot, "..", "skill-mining") });
|
|
99
|
+
smCandidates.push({ dir: path.resolve(tgtRoot, "..", "skill-mining") });
|
|
100
|
+
for (const candidate of smCandidates) {
|
|
101
|
+
if (existsSync(path.join(candidate.dir, "bin", "cli.js"))) {
|
|
102
|
+
smMode = "dir";
|
|
103
|
+
smDir = candidate.dir;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (candidate.explicit) {
|
|
107
|
+
log.warn(
|
|
108
|
+
`DOBETTER_SKILL_MINING_DIR=${candidate.dir} does not look like a skill-mining checkout ` +
|
|
109
|
+
`(no bin/cli.js) — continuing probe.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (smMode === "absent" && !noNpx) smMode = "npx";
|
|
114
|
+
available["skill-mining"] = smMode !== "absent";
|
|
115
|
+
|
|
116
|
+
return { mode, dir, available, skillMining: { mode: smMode, dir: smDir } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Heuristic: did the spawn fail because npx could not resolve the package
|
|
120
|
+
// (vs the tool itself running and exiting non-zero)?
|
|
121
|
+
function npxResolveFailed(result) {
|
|
122
|
+
if (result?.error?.code === "ENOENT") return true; // npx binary itself absent
|
|
123
|
+
if (result?.status === 0) return false;
|
|
124
|
+
const stderr = result?.stderr ?? "";
|
|
125
|
+
return (
|
|
126
|
+
/npm (?:ERR!|error)/i.test(stderr) &&
|
|
127
|
+
/E404|ETARGET|ENOTFOUND|could not determine executable/i.test(stderr)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toToolResult(result, wantJson) {
|
|
132
|
+
const exitCode = Number.isInteger(result.status) ? result.status : null;
|
|
133
|
+
const stdout = result.stdout ?? "";
|
|
134
|
+
const stderr = result.stderr ?? "";
|
|
135
|
+
let json = null;
|
|
136
|
+
let parseFailed = false;
|
|
137
|
+
if (wantJson) {
|
|
138
|
+
try {
|
|
139
|
+
json = JSON.parse(stdout.trim());
|
|
140
|
+
} catch {
|
|
141
|
+
json = null;
|
|
142
|
+
parseFailed = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const ok = exitCode === 0;
|
|
146
|
+
const gateFailed = exitCode === 2;
|
|
147
|
+
// Universal adlc exit contract: 0 pass / 1 could-not-run / 2 gate failed.
|
|
148
|
+
// Anything that is neither pass nor gate-fail (incl. spawn failure / timeout
|
|
149
|
+
// → status null) is operational; an unparseable --json stdout is too.
|
|
150
|
+
const opError = (!ok && !gateFailed) || parseFailed;
|
|
151
|
+
return { skipped: false, ok, gateFailed, opError, exitCode, stdout, stderr, json };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function truncateForReason(text, max = 300) {
|
|
155
|
+
const t = String(text ?? "").trim();
|
|
156
|
+
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Spawn one adlc tool. Never throws for tool ABSENCE ({ skipped: true });
|
|
160
|
+
// throws OpError only for caller bugs (unknown tool name, malformed args).
|
|
161
|
+
// LLM key env vars pass through to the child via the inherited environment
|
|
162
|
+
// (parallax/coldstart autodetect ANTHROPIC → OPENAI → GEMINI themselves).
|
|
163
|
+
export function runAdlcTool(loc, name, args = [], { cwd, exec, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
164
|
+
if (!ADLC_TOOLS.includes(name)) {
|
|
165
|
+
throw new OpError(`Unknown adlc tool "${name}" (expected one of: ${ADLC_TOOLS.join(", ")}).`);
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(args) || args.some((a) => typeof a !== "string")) {
|
|
168
|
+
throw new OpError(`adlc ${name}: args must be an array of strings.`);
|
|
169
|
+
}
|
|
170
|
+
if (!loc || loc.mode === "absent" || !loc.available?.[name]) {
|
|
171
|
+
return { skipped: true, reason: REASON_NOT_INSTALLED };
|
|
172
|
+
}
|
|
173
|
+
const run = exec ?? makeExec();
|
|
174
|
+
let result;
|
|
175
|
+
if (loc.mode === "dir") {
|
|
176
|
+
const binPath = adlcBinPath(loc.dir, name);
|
|
177
|
+
if (!existsSync(binPath)) {
|
|
178
|
+
loc.available[name] = false; // drift since locate — flip for the session
|
|
179
|
+
return { skipped: true, reason: REASON_NOT_INSTALLED };
|
|
180
|
+
}
|
|
181
|
+
result = run(process.execPath, [binPath, ...args], { cwd, timeout: timeoutMs });
|
|
182
|
+
} else if (loc.mode === "npx") {
|
|
183
|
+
result = run("npx", ["--yes", `@adlc/${name}`, ...args], { cwd, timeout: timeoutMs });
|
|
184
|
+
if (npxResolveFailed(result)) {
|
|
185
|
+
loc.available[name] = false; // session flip — sanctioned service-object mutation
|
|
186
|
+
return { skipped: true, reason: `npx could not resolve @adlc/${name}` };
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
return { skipped: true, reason: REASON_NOT_INSTALLED };
|
|
190
|
+
}
|
|
191
|
+
return toToolResult(result, args.includes("--json"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// LLM-backed tools (parallax, coldstart) exit 1 when they cannot run at all —
|
|
195
|
+
// most commonly no API key in env. Blueprint §5: treat as absent for that run
|
|
196
|
+
// so callers apply the same declared degradation as for a missing tool.
|
|
197
|
+
function couldNotRun(name, r) {
|
|
198
|
+
return {
|
|
199
|
+
skipped: true,
|
|
200
|
+
reason: `${name} could not run (exit 1): ${truncateForReason(r.stderr || r.stdout) || "no output"}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// D1 divergence gate. gate === true ⇔ exit 0 (divergence below threshold).
|
|
205
|
+
export function runParallax(loc, { file, request, n = 3, threshold = 0.25, cwd, exec } = {}) {
|
|
206
|
+
if (!file && !request) {
|
|
207
|
+
throw new OpError("runParallax requires either { file } or { request }.");
|
|
208
|
+
}
|
|
209
|
+
const subject = file ? ["--file", file] : ["--request", request];
|
|
210
|
+
const args = [...subject, "--n", String(n), "--threshold", String(threshold), "--json"];
|
|
211
|
+
const r = runAdlcTool(loc, "parallax", args, { cwd, exec });
|
|
212
|
+
if (r.skipped) return r;
|
|
213
|
+
if (r.exitCode === 1) return couldNotRun("parallax", r);
|
|
214
|
+
const j = r.json ?? {};
|
|
215
|
+
return {
|
|
216
|
+
skipped: false,
|
|
217
|
+
gate: r.exitCode === 0,
|
|
218
|
+
score: typeof j.score === "number" ? j.score : null,
|
|
219
|
+
agreements: Array.isArray(j.agreements) ? j.agreements : [],
|
|
220
|
+
divergences: Array.isArray(j.divergences) ? j.divergences : [],
|
|
221
|
+
raw: r.json,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// D3 coldstart gate over .dobetter/backlog/tickets.json (byte-compatible with
|
|
226
|
+
// aidlc's .adlc/tickets.json schema).
|
|
227
|
+
export function runColdstart(loc, { ticketsPath, all = true, ticketId, cwd, exec } = {}) {
|
|
228
|
+
if (typeof ticketsPath !== "string" || ticketsPath.length === 0) {
|
|
229
|
+
throw new OpError("runColdstart requires { ticketsPath }.");
|
|
230
|
+
}
|
|
231
|
+
if (!all && (typeof ticketId !== "string" || ticketId.length === 0)) {
|
|
232
|
+
throw new OpError("runColdstart requires { ticketId } when all is false.");
|
|
233
|
+
}
|
|
234
|
+
const args = all
|
|
235
|
+
? ["--all", "--tickets", ticketsPath, "--json"]
|
|
236
|
+
: [ticketId, "--tickets", ticketsPath, "--json"];
|
|
237
|
+
const r = runAdlcTool(loc, "coldstart", args, { cwd, exec });
|
|
238
|
+
if (r.skipped) return r;
|
|
239
|
+
if (r.exitCode === 1) return couldNotRun("coldstart", r);
|
|
240
|
+
const j = r.json ?? {};
|
|
241
|
+
return {
|
|
242
|
+
skipped: false,
|
|
243
|
+
ok: r.exitCode === 0,
|
|
244
|
+
results: Array.isArray(j.results) ? j.results : [],
|
|
245
|
+
raw: r.json,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// D4 hollow-test audit. cwd must be the TARGET repo.
|
|
250
|
+
export function runHollowTest(loc, { testCmd, base, max = 20, cwd, exec } = {}) {
|
|
251
|
+
if (typeof testCmd !== "string" || testCmd.length === 0) {
|
|
252
|
+
throw new OpError("runHollowTest requires { testCmd }.");
|
|
253
|
+
}
|
|
254
|
+
if (typeof base !== "string" || base.length === 0) {
|
|
255
|
+
throw new OpError("runHollowTest requires { base }.");
|
|
256
|
+
}
|
|
257
|
+
const args = ["--test-cmd", testCmd, "--base", base, "--max", String(max), "--json"];
|
|
258
|
+
const r = runAdlcTool(loc, "hollow-test", args, { cwd, exec });
|
|
259
|
+
if (r.skipped) return r;
|
|
260
|
+
const j = r.json ?? {};
|
|
261
|
+
const summary =
|
|
262
|
+
j.summary && typeof j.summary === "object" ? j.summary : { total: 0, killed: 0, survived: 0 };
|
|
263
|
+
return {
|
|
264
|
+
skipped: false,
|
|
265
|
+
ok: r.ok,
|
|
266
|
+
gateFailed: r.gateFailed,
|
|
267
|
+
opError: r.opError,
|
|
268
|
+
summary,
|
|
269
|
+
mutants: Array.isArray(j.mutants) ? j.mutants : [],
|
|
270
|
+
raw: r.json,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// refresh regression detection. verb: "capture" | "compare"; args are the
|
|
275
|
+
// verb's own argv (e.g. ["--config", cfg, "--out", out, "--json"]).
|
|
276
|
+
export function runBehaviorDiff(loc, verb, args = [], { cwd, exec } = {}) {
|
|
277
|
+
if (verb !== "capture" && verb !== "compare") {
|
|
278
|
+
throw new OpError(`runBehaviorDiff verb must be "capture" or "compare", got "${verb}".`);
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(args) || args.some((a) => typeof a !== "string")) {
|
|
281
|
+
throw new OpError("runBehaviorDiff: args must be an array of strings.");
|
|
282
|
+
}
|
|
283
|
+
return runAdlcTool(loc, "behavior-diff", [verb, ...args], { cwd, exec });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// D4 env check. cwd must be the TARGET repo (preflight requires a git repo).
|
|
287
|
+
export function runPreflight(loc, { testCmd, cwd, exec } = {}) {
|
|
288
|
+
const args = ["--json", ...(testCmd ? ["--test-cmd", testCmd] : [])];
|
|
289
|
+
const r = runAdlcTool(loc, "preflight", args, { cwd, exec });
|
|
290
|
+
if (r.skipped) return r;
|
|
291
|
+
const j = r.json ?? {};
|
|
292
|
+
return {
|
|
293
|
+
skipped: false,
|
|
294
|
+
ok: r.ok,
|
|
295
|
+
gateFailed: r.gateFailed,
|
|
296
|
+
opError: r.opError,
|
|
297
|
+
checks: Array.isArray(j.checks) ? j.checks : [],
|
|
298
|
+
verdict: typeof j.verdict === "string" ? j.verdict : r.ok ? "pass" : "fail",
|
|
299
|
+
failedNames: Array.isArray(j.failedNames) ? j.failedNames : [],
|
|
300
|
+
raw: r.json,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// D1 sub-step: mine latent skills from the target repo.
|
|
305
|
+
export function runSkillMining(loc, { targetDir, offline = false, exec } = {}) {
|
|
306
|
+
if (typeof targetDir !== "string" || targetDir.length === 0) {
|
|
307
|
+
throw new OpError("runSkillMining requires { targetDir }.");
|
|
308
|
+
}
|
|
309
|
+
const sm = loc?.skillMining ?? { mode: "absent", dir: null };
|
|
310
|
+
if (!loc?.available?.["skill-mining"] || sm.mode === "absent") {
|
|
311
|
+
return { skipped: true, reason: REASON_NOT_INSTALLED };
|
|
312
|
+
}
|
|
313
|
+
const run = exec ?? makeExec();
|
|
314
|
+
const tail = ["mine", targetDir, ...(offline ? ["--offline"] : [])];
|
|
315
|
+
let result;
|
|
316
|
+
if (sm.mode === "dir") {
|
|
317
|
+
const binPath = path.join(sm.dir, "bin", "cli.js");
|
|
318
|
+
if (!existsSync(binPath)) {
|
|
319
|
+
loc.available["skill-mining"] = false;
|
|
320
|
+
return { skipped: true, reason: REASON_NOT_INSTALLED };
|
|
321
|
+
}
|
|
322
|
+
result = run(process.execPath, [binPath, ...tail], { timeout: DEFAULT_TIMEOUT_MS });
|
|
323
|
+
} else {
|
|
324
|
+
result = run("npx", ["--yes", "skill-mining", ...tail], { timeout: DEFAULT_TIMEOUT_MS });
|
|
325
|
+
if (npxResolveFailed(result)) {
|
|
326
|
+
loc.available["skill-mining"] = false;
|
|
327
|
+
return { skipped: true, reason: "npx could not resolve skill-mining" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { skipped: false, ok: result.status === 0, stdout: result.stdout ?? "" };
|
|
331
|
+
}
|