@skill-harness/core 0.1.2 → 0.3.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/types.d.ts +6 -0
- package/dist/discover.d.ts +5 -1
- package/dist/discover.js +9 -1
- package/dist/grade.d.ts +14 -1
- package/dist/grade.js +74 -11
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/journal.d.ts +14 -0
- package/dist/lift.d.ts +69 -0
- package/dist/lift.js +163 -0
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +168 -4
- package/dist/regrade.d.ts +6 -0
- package/dist/regrade.js +25 -3
- package/dist/report.d.ts +14 -0
- package/dist/report.js +16 -1
- package/dist/rescore.d.ts +34 -0
- package/dist/rescore.js +66 -0
- package/dist/results.d.ts +41 -9
- package/dist/results.js +58 -14
- package/dist/run.d.ts +25 -1
- package/dist/run.js +115 -22
- package/dist/scaffold.d.ts +28 -0
- package/dist/scaffold.js +188 -0
- package/dist/score.d.ts +5 -1
- package/dist/seeded.d.ts +83 -3
- package/dist/seeded.js +315 -15
- package/dist/sources.d.ts +115 -0
- package/dist/sources.js +253 -0
- package/dist/spec.d.ts +17 -0
- package/dist/spec.js +63 -0
- package/dist/util/env.d.ts +32 -0
- package/dist/util/env.js +67 -0
- package/dist/workspace.d.ts +33 -1
- package/dist/workspace.js +125 -6
- package/package.json +1 -1
package/dist/workspace.js
CHANGED
|
@@ -1,36 +1,155 @@
|
|
|
1
|
-
import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
const GIT_TIMEOUT_MS = 30_000;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Fixture subdirectories copied over the workspace AFTER the baseline commit, so their
|
|
8
|
+
* files land as pending changes rather than history. Lets a scenario start from a dirty
|
|
9
|
+
* tree — "I fixed the typo, commit it" needs the fix present but not yet committed, and
|
|
10
|
+
* "commit my staged changes" needs a populated index. `_staged/` contents are added to
|
|
11
|
+
* the index, `_uncommitted/` contents are left unstaged; a fixture may use either or
|
|
12
|
+
* both. Neither marker directory ever appears in the workspace.
|
|
13
|
+
*/
|
|
14
|
+
const UNCOMMITTED_DIR = "_uncommitted";
|
|
15
|
+
const STAGED_DIR = "_staged";
|
|
16
|
+
export const MARKERS = [STAGED_DIR, UNCOMMITTED_DIR];
|
|
17
|
+
/**
|
|
18
|
+
* Top-level directories in a fixture that claim to be markers but aren't one.
|
|
19
|
+
*
|
|
20
|
+
* A misspelled marker (`_uncommited/`) used to be copied into the baseline commit as a
|
|
21
|
+
* literal directory: the tree came up clean, the scenario silently measured the opposite
|
|
22
|
+
* of its intent, and nothing reported it. Any top-level `_name/` is therefore treated as
|
|
23
|
+
* a marker claim and must be a real one.
|
|
24
|
+
*
|
|
25
|
+
* The predicate is `_` followed by a LETTER, so `__tests__` and `__pycache__` — ordinary
|
|
26
|
+
* directories a fixture may legitimately contain — are not marker claims, and neither are
|
|
27
|
+
* oddities like `_2fa/` or `_-tmp/`, which no marker could plausibly be mistaken for.
|
|
28
|
+
* Nested `pkg/_staged/` is likewise ordinary content: markers are top-level only.
|
|
29
|
+
*
|
|
30
|
+
* Exported so `lint` can report the same set this module refuses to run. If the two
|
|
31
|
+
* disagreed, lint would hand out a clean bill of health for a fixture the runtime then
|
|
32
|
+
* rejects — which is worse than not checking at all.
|
|
33
|
+
*/
|
|
34
|
+
export function unknownMarkerDirs(src) {
|
|
35
|
+
return readdirSync(src, { withFileTypes: true })
|
|
36
|
+
.filter((e) => e.isDirectory() && /^_[A-Za-z]/.test(e.name) && !MARKERS.includes(e.name))
|
|
37
|
+
.map((e) => e.name)
|
|
38
|
+
.sort();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The known marker a misspelling was probably reaching for, or null.
|
|
42
|
+
*
|
|
43
|
+
* Case-insensitive within an edit distance of 2 — enough for `_uncommited` (one
|
|
44
|
+
* dropped `t`), `_Staged` (case) and `_uncommmitted` (doubled letter), while
|
|
45
|
+
* `_fixtures` or `_helpers` correctly suggest nothing rather than a confident
|
|
46
|
+
* wrong guess.
|
|
47
|
+
*/
|
|
48
|
+
export function suggestMarker(name) {
|
|
49
|
+
let best = null;
|
|
50
|
+
let bestDistance = 3;
|
|
51
|
+
for (const m of MARKERS) {
|
|
52
|
+
const d = editDistance(name.toLowerCase(), m.toLowerCase());
|
|
53
|
+
if (d < bestDistance) {
|
|
54
|
+
bestDistance = d;
|
|
55
|
+
best = m;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
60
|
+
/** Levenshtein distance, iterative single-row. */
|
|
61
|
+
function editDistance(a, b) {
|
|
62
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
63
|
+
for (let i = 1; i <= a.length; i++) {
|
|
64
|
+
const curr = [i];
|
|
65
|
+
for (let j = 1; j <= b.length; j++) {
|
|
66
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
67
|
+
}
|
|
68
|
+
prev = curr;
|
|
69
|
+
}
|
|
70
|
+
return prev[b.length];
|
|
71
|
+
}
|
|
72
|
+
function assertKnownMarkers(src) {
|
|
73
|
+
const suspects = unknownMarkerDirs(src);
|
|
74
|
+
if (suspects.length > 0) {
|
|
75
|
+
throw new Error(`fixture ${src}: unknown marker director${suspects.length > 1 ? "ies" : "y"} ` +
|
|
76
|
+
`${suspects.map((s) => `\`${s}/\``).join(", ")} — known markers are ` +
|
|
77
|
+
`${MARKERS.map((m) => `\`${m}/\``).join(" and ")}. Rename it, or move it deeper if it is ordinary content.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* git init + a baseline commit, so a later `git diff --cached` shows only edits.
|
|
82
|
+
* Pinned to `main`: the host's init.defaultBranch is not ours to depend on, and
|
|
83
|
+
* scenarios say things like "I'm on the main branch".
|
|
84
|
+
*/
|
|
7
85
|
function gitBaseline(cwd) {
|
|
8
|
-
execFileSync("git", ["init", "-q"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
86
|
+
execFileSync("git", ["init", "-q", "-b", "main"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
9
87
|
execFileSync("git", ["add", "-A"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
10
88
|
execFileSync("git", ["-c", "user.email=sh@local", "-c", "user.name=skill-harness", "commit", "-q", "--allow-empty", "-m", "baseline"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
11
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Wire a bare repo in its own temp dir as `origin` and push the baseline, so `main`
|
|
92
|
+
* tracks `origin/main` and the baseline commit genuinely exists upstream.
|
|
93
|
+
*
|
|
94
|
+
* Without this, a git fixture has no remote — and a model reasonably reads a missing
|
|
95
|
+
* upstream as "solo throwaway repo", which is how git-ops A4 passes or fails depending
|
|
96
|
+
* on the run. A real remote makes "shared work" a fact of the fixture rather than a
|
|
97
|
+
* claim in the prompt, and needs no network.
|
|
98
|
+
*/
|
|
99
|
+
function addLocalRemote(cwd) {
|
|
100
|
+
const bare = mkdtempSync(join(tmpdir(), "sc-remote-")) + ".git";
|
|
101
|
+
execFileSync("git", ["init", "-q", "--bare", "-b", "main", bare], { timeout: GIT_TIMEOUT_MS });
|
|
102
|
+
execFileSync("git", ["remote", "add", "origin", bare], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
103
|
+
execFileSync("git", ["push", "-q", "-u", "origin", "main"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
104
|
+
return bare;
|
|
105
|
+
}
|
|
12
106
|
/**
|
|
13
107
|
* Create an isolated temp-dir working directory for one scenario. `none` is an
|
|
14
108
|
* empty dir (no git); `empty-git` initialises a clean repo; `{ fixture }` copies
|
|
15
109
|
* the fixture (relative paths resolve against `specDir`) then initialises a repo
|
|
16
|
-
* with a baseline commit.
|
|
110
|
+
* with a baseline commit. A fixture may carry top-level `_staged/` and
|
|
111
|
+
* `_uncommitted/` subdirectories, applied after that commit to start the scenario
|
|
112
|
+
* from a dirty tree. `opts.remote` additionally wires a local bare `origin`.
|
|
113
|
+
* Child processes run here, never in the user's home.
|
|
17
114
|
*/
|
|
18
115
|
export function createWorkspace(kind, opts) {
|
|
19
116
|
const cwd = mkdtempSync(join(tmpdir(), "sc-ws-"));
|
|
20
|
-
|
|
117
|
+
let bare = null;
|
|
118
|
+
const cleanup = () => {
|
|
119
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
120
|
+
if (bare)
|
|
121
|
+
rmSync(bare, { recursive: true, force: true }); // the remote is ours too
|
|
122
|
+
};
|
|
21
123
|
try {
|
|
22
124
|
if (kind === "none") {
|
|
23
125
|
// empty isolated dir; nothing to set up
|
|
24
126
|
}
|
|
25
127
|
else if (kind === "empty-git") {
|
|
26
128
|
gitBaseline(cwd);
|
|
129
|
+
if (opts.remote)
|
|
130
|
+
bare = addLocalRemote(cwd);
|
|
27
131
|
}
|
|
28
132
|
else {
|
|
29
133
|
const src = isAbsolute(kind.fixture) ? kind.fixture : resolve(opts.specDir, kind.fixture);
|
|
30
134
|
if (!existsSync(src))
|
|
31
135
|
throw new Error(`fixture not found: ${src}`);
|
|
32
|
-
|
|
136
|
+
assertKnownMarkers(src);
|
|
137
|
+
const pending = [STAGED_DIR, UNCOMMITTED_DIR].map((d) => join(src, d));
|
|
138
|
+
cpSync(src, cwd, {
|
|
139
|
+
recursive: true,
|
|
140
|
+
filter: (from) => !pending.includes(from), // committed baseline only
|
|
141
|
+
});
|
|
33
142
|
gitBaseline(cwd);
|
|
143
|
+
// Before the pending changes land, so the remote holds the baseline only.
|
|
144
|
+
if (opts.remote)
|
|
145
|
+
bare = addLocalRemote(cwd);
|
|
146
|
+
const [staged, uncommitted] = pending;
|
|
147
|
+
if (existsSync(staged)) {
|
|
148
|
+
cpSync(staged, cwd, { recursive: true });
|
|
149
|
+
execFileSync("git", ["add", "-A"], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
150
|
+
}
|
|
151
|
+
if (existsSync(uncommitted))
|
|
152
|
+
cpSync(uncommitted, cwd, { recursive: true });
|
|
34
153
|
}
|
|
35
154
|
}
|
|
36
155
|
catch (e) {
|