@inkobytes/nexus 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 +455 -0
- package/bin/nexus.js +108 -0
- package/drills/nexus-agent-protocol/README.md +65 -0
- package/drills/nexus-agent-protocol/cases/blocked.yaml +20 -0
- package/drills/nexus-agent-protocol/cases/claim-before-edit.yaml +16 -0
- package/drills/nexus-agent-protocol/cases/current-file-state.yaml +15 -0
- package/drills/nexus-agent-protocol/cases/data-boundary-table-header.yaml +21 -0
- package/drills/nexus-agent-protocol/cases/data-mutation-delete-rows.yaml +20 -0
- package/drills/nexus-agent-protocol/cases/done-claim-adversarial.yaml +18 -0
- package/drills/nexus-agent-protocol/cases/ghost-file-claim-loop.yaml +16 -0
- package/drills/nexus-agent-protocol/cases/issue-found.yaml +21 -0
- package/drills/nexus-agent-protocol/cases/private-path-protection.yaml +23 -0
- package/drills/nexus-agent-protocol/cases/queue-is-thin-index.yaml +21 -0
- package/drills/nexus-agent-protocol/cases/removal-scope.yaml +26 -0
- package/drills/nexus-agent-protocol/cases/remove-agent-folders-from-git.yaml +24 -0
- package/drills/nexus-agent-protocol/cases/stale-lock-after-commit.yaml +26 -0
- package/drills/nexus-agent-protocol/cases/start-does-not-replace-claim-release.yaml +17 -0
- package/drills/nexus-agent-protocol/cases/task-contract.yaml +23 -0
- package/drills/nexus-agent-protocol/cases/vendor-cleanup-preserve-history.yaml +24 -0
- package/drills/nexus-agent-protocol/cases/wrong-repo-push.yaml +23 -0
- package/nexus-dashboard/docs/index.html +183 -0
- package/nexus-dashboard/index.html +678 -0
- package/nexus-dashboard/logo-nexus.svg +14 -0
- package/nexus-dashboard/style.css +1454 -0
- package/package.json +42 -0
- package/skills/nexus/SKILL.md +62 -0
- package/src/commands/checkin.js +19 -0
- package/src/commands/checkout.js +33 -0
- package/src/commands/chmod.js +93 -0
- package/src/commands/claim.js +122 -0
- package/src/commands/clean.js +76 -0
- package/src/commands/dashboard.js +387 -0
- package/src/commands/db.js +256 -0
- package/src/commands/doctor.js +958 -0
- package/src/commands/drill.js +507 -0
- package/src/commands/help.js +8 -0
- package/src/commands/init.js +576 -0
- package/src/commands/ledger.js +215 -0
- package/src/commands/metrics.js +178 -0
- package/src/commands/next.js +317 -0
- package/src/commands/release.js +107 -0
- package/src/commands/soul.js +156 -0
- package/src/commands/standup.js +59 -0
- package/src/commands/start.js +126 -0
- package/src/commands/status.js +109 -0
- package/src/hooks/pre-migration-backup.js +35 -0
- package/src/lib/agentScopes.js +61 -0
- package/src/lib/blackboard.js +90 -0
- package/src/lib/config.js +38 -0
- package/src/lib/dump.js +63 -0
- package/src/lib/git.js +111 -0
- package/src/lib/lockManager.js +302 -0
- package/src/lib/pathSafety.js +41 -0
- package/src/lib/permissions.js +74 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nexus drill <list|show|run|report> [id]
|
|
3
|
+
* Protocol drills for known shared-repo failure modes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { getConfig } from '../lib/config.js';
|
|
10
|
+
|
|
11
|
+
const DRILL_ROOT = 'drills/nexus-agent-protocol';
|
|
12
|
+
const RUNS_ROOT = '.nexus/drill-runs';
|
|
13
|
+
const COMMAND_DIR = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const VALID_STATUSES = new Set(['pass', 'fail', 'needs_review']);
|
|
15
|
+
|
|
16
|
+
export default function drill(args) {
|
|
17
|
+
const action = args[0] || 'list';
|
|
18
|
+
const rest = args.slice(1);
|
|
19
|
+
|
|
20
|
+
switch (action) {
|
|
21
|
+
case 'list':
|
|
22
|
+
listDrills();
|
|
23
|
+
break;
|
|
24
|
+
case 'show':
|
|
25
|
+
requireId(action, rest[0]);
|
|
26
|
+
showDrill(rest[0]);
|
|
27
|
+
break;
|
|
28
|
+
case 'run':
|
|
29
|
+
runDrills(rest);
|
|
30
|
+
break;
|
|
31
|
+
case 'report':
|
|
32
|
+
reportDrills();
|
|
33
|
+
break;
|
|
34
|
+
case '--help':
|
|
35
|
+
case '-h':
|
|
36
|
+
case 'help':
|
|
37
|
+
printHelp();
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
console.error(`Unknown drill action: ${action}`);
|
|
41
|
+
printHelp();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listDrills() {
|
|
47
|
+
const drills = loadDrills();
|
|
48
|
+
|
|
49
|
+
if (drills.length === 0) {
|
|
50
|
+
console.log('No protocol drills found.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('Nexus protocol drills:');
|
|
55
|
+
for (const item of drills) {
|
|
56
|
+
console.log(`- ${item.id} - ${item.description}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function showDrill(id) {
|
|
61
|
+
const item = findDrill(id);
|
|
62
|
+
console.log(item.raw.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function runDrills(args) {
|
|
66
|
+
const options = parseRunOptions(args);
|
|
67
|
+
const drills = options.id ? [findDrill(options.id)] : loadDrills();
|
|
68
|
+
|
|
69
|
+
if (drills.length === 0) {
|
|
70
|
+
console.log('No protocol drills found.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const run = createRunArtifacts(drills, options);
|
|
75
|
+
const counts = countStatuses(run.results);
|
|
76
|
+
|
|
77
|
+
console.log('# Nexus Protocol Drill Run');
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(`Run: ${run.id}`);
|
|
80
|
+
console.log(`Artifacts: ${run.relativeDir}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(`Total: ${run.results.length}`);
|
|
83
|
+
console.log(`Passed: ${counts.pass}`);
|
|
84
|
+
console.log(`Failed: ${counts.fail}`);
|
|
85
|
+
console.log(`Needs Review: ${counts.needs_review}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function reportDrills() {
|
|
89
|
+
const latest = findLatestRun();
|
|
90
|
+
|
|
91
|
+
console.log('# Nexus Protocol Drill Report');
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
if (!latest) {
|
|
95
|
+
console.log('Latest results: none recorded yet.');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('Run `nexus drill run` to create .nexus/drill-runs artifacts.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(readFileSync(join(latest.absoluteDir, 'results.json'), 'utf-8'));
|
|
104
|
+
} catch {
|
|
105
|
+
console.log('Latest results: unreadable results file.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
110
|
+
const counts = countStatuses(results);
|
|
111
|
+
|
|
112
|
+
console.log(`Run: ${data.run_id || latest.id}`);
|
|
113
|
+
console.log(`Artifacts: ${latest.relativeDir}`);
|
|
114
|
+
if (data.ran_at) console.log(`Ran at: ${data.ran_at}`);
|
|
115
|
+
if (data.agent) console.log(`Agent: ${data.agent}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(`Total: ${results.length}`);
|
|
118
|
+
console.log(`Passed: ${counts.pass}`);
|
|
119
|
+
console.log(`Failed: ${counts.fail}`);
|
|
120
|
+
console.log(`Needs Review: ${counts.needs_review}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
|
|
123
|
+
printResultGroup('Failed', results.filter(result => result.status === 'fail'));
|
|
124
|
+
printResultGroup('Needs Review', results.filter(result => result.status === 'needs_review'));
|
|
125
|
+
printResultGroup('Passed', results.filter(result => result.status === 'pass'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createRunArtifacts(drills, options) {
|
|
129
|
+
const config = getConfig();
|
|
130
|
+
const runId = formatRunId(new Date());
|
|
131
|
+
const relativeDir = `${RUNS_ROOT}/${runId}`;
|
|
132
|
+
const absoluteDir = join(config.root, RUNS_ROOT, runId);
|
|
133
|
+
mkdirSync(absoluteDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const input = options.input ? readResultInput(options.input) : {};
|
|
136
|
+
const inputResults = normalizeInputResults(input);
|
|
137
|
+
validateInputResults(drills, inputResults, Boolean(options.input));
|
|
138
|
+
const results = drills.map(item => buildResult(item, inputResults.get(item.id), options, input));
|
|
139
|
+
|
|
140
|
+
for (const result of results) {
|
|
141
|
+
writeFileSync(join(absoluteDir, `${result.id}.json`), `${JSON.stringify(result, null, 2)}\n`, 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const payload = {
|
|
145
|
+
run_id: runId,
|
|
146
|
+
ran_at: new Date().toISOString(),
|
|
147
|
+
agent: options.agent || input.agent || process.env.NEXUS_AGENT || null,
|
|
148
|
+
judge: options.judge || input.judge || 'manual',
|
|
149
|
+
source: options.input || null,
|
|
150
|
+
results,
|
|
151
|
+
};
|
|
152
|
+
writeFileSync(join(absoluteDir, 'results.json'), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
153
|
+
writeFileSync(join(absoluteDir, 'report.md'), renderReport(payload), 'utf-8');
|
|
154
|
+
|
|
155
|
+
return { id: runId, relativeDir, absoluteDir, results };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseRunOptions(args) {
|
|
159
|
+
const options = {
|
|
160
|
+
id: null,
|
|
161
|
+
input: null,
|
|
162
|
+
agent: null,
|
|
163
|
+
judge: null,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
167
|
+
const arg = args[index];
|
|
168
|
+
if (arg === '--input' || arg === '--results') {
|
|
169
|
+
options.input = requireValue(args, index, arg);
|
|
170
|
+
index += 1;
|
|
171
|
+
} else if (arg === '--agent') {
|
|
172
|
+
options.agent = requireValue(args, index, arg);
|
|
173
|
+
index += 1;
|
|
174
|
+
} else if (arg === '--judge') {
|
|
175
|
+
options.judge = requireValue(args, index, arg);
|
|
176
|
+
index += 1;
|
|
177
|
+
} else if (arg.startsWith('--')) {
|
|
178
|
+
throw new Error(`Unknown drill run option: ${arg}`);
|
|
179
|
+
} else if (!options.id) {
|
|
180
|
+
options.id = arg;
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error(`Unexpected drill run argument: ${arg}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return options;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function requireValue(args, index, flag) {
|
|
190
|
+
const value = args[index + 1];
|
|
191
|
+
if (!value || value.startsWith('--')) {
|
|
192
|
+
throw new Error(`Missing value for ${flag}`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readResultInput(inputPath) {
|
|
198
|
+
const absolutePath = join(getConfig().root, inputPath);
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
throw new Error(`Could not read drill result input ${inputPath}: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeInputResults(input) {
|
|
207
|
+
if (input.id) {
|
|
208
|
+
validateResultShape(input, 'result');
|
|
209
|
+
return new Map([[input.id, input]]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const rawResults = Array.isArray(input)
|
|
213
|
+
? input
|
|
214
|
+
: Array.isArray(input.results)
|
|
215
|
+
? input.results
|
|
216
|
+
: Object.entries(input.results || {}).map(([id, value]) => ({ id, ...value }));
|
|
217
|
+
|
|
218
|
+
const mapped = new Map();
|
|
219
|
+
rawResults.forEach((result, index) => {
|
|
220
|
+
validateResultShape(result, `results[${index}]`);
|
|
221
|
+
if (mapped.has(result.id)) {
|
|
222
|
+
throw new Error(`Duplicate drill result id: ${result.id}`);
|
|
223
|
+
}
|
|
224
|
+
mapped.set(result.id, result);
|
|
225
|
+
});
|
|
226
|
+
return mapped;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateInputResults(drills, inputResults, inputProvided) {
|
|
230
|
+
const knownIds = new Set(drills.map(item => item.id));
|
|
231
|
+
|
|
232
|
+
for (const id of inputResults.keys()) {
|
|
233
|
+
if (!knownIds.has(id)) {
|
|
234
|
+
throw new Error(`Result input contains unknown drill id: ${id}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!inputProvided) return;
|
|
239
|
+
|
|
240
|
+
const missing = drills
|
|
241
|
+
.filter(item => !inputResults.has(item.id))
|
|
242
|
+
.map(item => item.id);
|
|
243
|
+
|
|
244
|
+
if (missing.length > 0) {
|
|
245
|
+
console.warn(`[WARN] Missing result input for drill(s): ${missing.join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function validateResultShape(result, label) {
|
|
250
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
251
|
+
throw new Error(`Invalid drill result at ${label}: expected object`);
|
|
252
|
+
}
|
|
253
|
+
if (!result.id || typeof result.id !== 'string') {
|
|
254
|
+
throw new Error(`Invalid drill result at ${label}: missing string id`);
|
|
255
|
+
}
|
|
256
|
+
if (result.status !== undefined && !VALID_STATUSES.has(result.status)) {
|
|
257
|
+
throw new Error(`Invalid status for ${result.id}: ${result.status}`);
|
|
258
|
+
}
|
|
259
|
+
validateStringArray(result, 'matched_expected');
|
|
260
|
+
validateStringArray(result, 'matched_fail_if');
|
|
261
|
+
if (result.notes !== undefined && typeof result.notes !== 'string') {
|
|
262
|
+
throw new Error(`Invalid notes for ${result.id}: expected string`);
|
|
263
|
+
}
|
|
264
|
+
if (result.judge !== undefined && typeof result.judge !== 'string') {
|
|
265
|
+
throw new Error(`Invalid judge for ${result.id}: expected string`);
|
|
266
|
+
}
|
|
267
|
+
if (result.confidence !== undefined && (
|
|
268
|
+
typeof result.confidence !== 'number' ||
|
|
269
|
+
result.confidence < 0 ||
|
|
270
|
+
result.confidence > 1
|
|
271
|
+
)) {
|
|
272
|
+
throw new Error(`Invalid confidence for ${result.id}: expected number from 0 to 1`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function validateStringArray(result, field) {
|
|
277
|
+
if (result[field] === undefined) return;
|
|
278
|
+
if (!Array.isArray(result[field]) || result[field].some(item => typeof item !== 'string')) {
|
|
279
|
+
throw new Error(`Invalid ${field} for ${result.id}: expected string array`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildResult(drillCase, inputResult, options, input) {
|
|
284
|
+
const matchedExpected = normalizeMatches(inputResult?.matched_expected);
|
|
285
|
+
const matchedFailIf = normalizeMatches(inputResult?.matched_fail_if);
|
|
286
|
+
const unmatchedExpected = drillCase.expected.filter(expected => !matchedExpected.includes(expected));
|
|
287
|
+
const status = decideStatus(inputResult?.status, matchedExpected, matchedFailIf, unmatchedExpected);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
id: drillCase.id,
|
|
291
|
+
status,
|
|
292
|
+
matched_expected: matchedExpected,
|
|
293
|
+
unmatched_expected: unmatchedExpected,
|
|
294
|
+
matched_fail_if: matchedFailIf,
|
|
295
|
+
notes: inputResult?.notes || defaultNotes(status, inputResult),
|
|
296
|
+
judge: inputResult?.judge || options.judge || input.judge || 'manual',
|
|
297
|
+
confidence: typeof inputResult?.confidence === 'number' ? inputResult.confidence : null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeMatches(value) {
|
|
302
|
+
if (!Array.isArray(value)) return [];
|
|
303
|
+
return value.filter(item => typeof item === 'string');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function decideStatus(requestedStatus, matchedExpected, matchedFailIf, unmatchedExpected) {
|
|
307
|
+
if (matchedFailIf.length > 0 || requestedStatus === 'fail') return 'fail';
|
|
308
|
+
if (requestedStatus === 'needs_review') return 'needs_review';
|
|
309
|
+
if (requestedStatus === 'pass' || (matchedExpected.length > 0 && unmatchedExpected.length === 0)) return 'pass';
|
|
310
|
+
return 'needs_review';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function defaultNotes(status, inputResult) {
|
|
314
|
+
if (!inputResult) return 'Missing result input for this drill.';
|
|
315
|
+
if (status === 'fail') return 'A fail_if condition triggered.';
|
|
316
|
+
if (status === 'pass') return 'Expected behavior satisfied and no fail_if condition triggered.';
|
|
317
|
+
return 'Expected behavior is incomplete or judge confidence is low.';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function findLatestRun() {
|
|
321
|
+
const config = getConfig();
|
|
322
|
+
const runsDir = join(config.root, RUNS_ROOT);
|
|
323
|
+
if (!existsSync(runsDir)) return null;
|
|
324
|
+
|
|
325
|
+
const runs = readdirSync(runsDir, { withFileTypes: true })
|
|
326
|
+
.filter(entry => entry.isDirectory())
|
|
327
|
+
.map(entry => ({
|
|
328
|
+
id: entry.name,
|
|
329
|
+
relativeDir: `${RUNS_ROOT}/${entry.name}`,
|
|
330
|
+
absoluteDir: join(runsDir, entry.name),
|
|
331
|
+
}))
|
|
332
|
+
.filter(entry => existsSync(join(entry.absoluteDir, 'results.json')))
|
|
333
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
334
|
+
|
|
335
|
+
return runs[runs.length - 1] || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function countStatuses(results) {
|
|
339
|
+
return results.reduce((counts, result) => {
|
|
340
|
+
const status = result.status || 'needs_review';
|
|
341
|
+
if (status === 'pass') counts.pass += 1;
|
|
342
|
+
else if (status === 'fail') counts.fail += 1;
|
|
343
|
+
else counts.needs_review += 1;
|
|
344
|
+
return counts;
|
|
345
|
+
}, { pass: 0, fail: 0, needs_review: 0 });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function printResultGroup(label, results) {
|
|
349
|
+
if (results.length === 0) return;
|
|
350
|
+
|
|
351
|
+
console.log(`${label}:`);
|
|
352
|
+
for (const result of results) {
|
|
353
|
+
console.log(`- ${result.id}`);
|
|
354
|
+
if (result.notes) console.log(` Reason: ${result.notes}`);
|
|
355
|
+
}
|
|
356
|
+
console.log('');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function renderReport(payload) {
|
|
360
|
+
const counts = countStatuses(payload.results);
|
|
361
|
+
const lines = [
|
|
362
|
+
'# Drill Report',
|
|
363
|
+
'',
|
|
364
|
+
`Run: ${payload.run_id}`,
|
|
365
|
+
`Ran at: ${payload.ran_at}`,
|
|
366
|
+
'',
|
|
367
|
+
`Total: ${payload.results.length}`,
|
|
368
|
+
`Passed: ${counts.pass}`,
|
|
369
|
+
`Failed: ${counts.fail}`,
|
|
370
|
+
`Needs Review: ${counts.needs_review}`,
|
|
371
|
+
'',
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
appendReportGroup(lines, 'Failed', payload.results.filter(result => result.status === 'fail'));
|
|
375
|
+
appendReportGroup(lines, 'Needs Review', payload.results.filter(result => result.status === 'needs_review'));
|
|
376
|
+
appendReportGroup(lines, 'Passed', payload.results.filter(result => result.status === 'pass'));
|
|
377
|
+
|
|
378
|
+
return `${lines.join('\n')}\n`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function appendReportGroup(lines, label, results) {
|
|
382
|
+
if (results.length === 0) return;
|
|
383
|
+
|
|
384
|
+
lines.push(`## ${label}`);
|
|
385
|
+
lines.push('');
|
|
386
|
+
|
|
387
|
+
for (const result of results) {
|
|
388
|
+
lines.push(`- ${result.id}`);
|
|
389
|
+
lines.push(` - Reason: ${result.notes}`);
|
|
390
|
+
if (result.judge) lines.push(` - Judge: ${result.judge}`);
|
|
391
|
+
if (typeof result.confidence === 'number') lines.push(` - Confidence: ${result.confidence}`);
|
|
392
|
+
if (result.matched_fail_if?.length) {
|
|
393
|
+
lines.push(` - Matched fail_if: ${result.matched_fail_if.join(' | ')}`);
|
|
394
|
+
}
|
|
395
|
+
if (result.unmatched_expected?.length) {
|
|
396
|
+
lines.push(` - Missing expected: ${result.unmatched_expected.join(' | ')}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
lines.push('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function formatRunId(date) {
|
|
404
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function findDrill(id) {
|
|
408
|
+
const item = loadDrills().find(drillCase => drillCase.id === id);
|
|
409
|
+
if (!item) {
|
|
410
|
+
throw new Error(`Unknown drill: ${id}`);
|
|
411
|
+
}
|
|
412
|
+
return item;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function loadDrills() {
|
|
416
|
+
const casesDir = getDrillCasesDir();
|
|
417
|
+
if (!existsSync(casesDir)) return [];
|
|
418
|
+
|
|
419
|
+
return readdirSync(casesDir)
|
|
420
|
+
.filter(name => name.endsWith('.yaml') || name.endsWith('.yml'))
|
|
421
|
+
.sort()
|
|
422
|
+
.map(name => parseDrill(join(casesDir, name)));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function getDrillCasesDir() {
|
|
426
|
+
const localCasesDir = join(getConfig().root, DRILL_ROOT, 'cases');
|
|
427
|
+
if (existsSync(localCasesDir)) return localCasesDir;
|
|
428
|
+
|
|
429
|
+
return join(COMMAND_DIR, '..', '..', DRILL_ROOT, 'cases');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseDrill(filePath) {
|
|
433
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
434
|
+
const lines = raw.split('\n');
|
|
435
|
+
const id = readScalar(lines, 'id');
|
|
436
|
+
const prompt = unquote(readScalar(lines, 'prompt'));
|
|
437
|
+
const description = unquote(readScalar(lines, 'description')) || prompt;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
id,
|
|
441
|
+
description,
|
|
442
|
+
prompt,
|
|
443
|
+
raw,
|
|
444
|
+
setupLines: readBlock(lines, 'setup'),
|
|
445
|
+
expected: readList(lines, 'expected'),
|
|
446
|
+
failIf: readList(lines, 'fail_if'),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function readScalar(lines, key) {
|
|
451
|
+
const prefix = `${key}:`;
|
|
452
|
+
const line = lines.find(item => item.startsWith(prefix));
|
|
453
|
+
return line ? line.slice(prefix.length).trim() : '';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function readBlock(lines, key) {
|
|
457
|
+
const block = readRawBlock(lines, key);
|
|
458
|
+
return block
|
|
459
|
+
.map(line => line.trim())
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.map(line => line.replace(/^-\s*/, ''));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function readList(lines, key) {
|
|
465
|
+
return readRawBlock(lines, key)
|
|
466
|
+
.map(line => line.trim())
|
|
467
|
+
.filter(line => line.startsWith('- '))
|
|
468
|
+
.map(line => unquote(line.replace(/^-\s*/, '')));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function readRawBlock(lines, key) {
|
|
472
|
+
const start = lines.findIndex(line => line === `${key}:`);
|
|
473
|
+
if (start === -1) return [];
|
|
474
|
+
|
|
475
|
+
const block = [];
|
|
476
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
477
|
+
const line = lines[index];
|
|
478
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*:/.test(line)) break;
|
|
479
|
+
block.push(line);
|
|
480
|
+
}
|
|
481
|
+
return block;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function unquote(value) {
|
|
485
|
+
return value.replace(/^"(.*)"$/, '$1');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function requireId(action, id) {
|
|
489
|
+
if (!id) {
|
|
490
|
+
console.error(`Usage: nexus drill ${action} <id>`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function printHelp() {
|
|
496
|
+
console.log(`
|
|
497
|
+
Usage: nexus drill <list|show|run|report> [id]
|
|
498
|
+
|
|
499
|
+
Commands:
|
|
500
|
+
nexus drill list
|
|
501
|
+
nexus drill show wrong-repo-push
|
|
502
|
+
nexus drill run
|
|
503
|
+
nexus drill run wrong-repo-push
|
|
504
|
+
nexus drill run wrong-repo-push --input judge-results.json
|
|
505
|
+
nexus drill report
|
|
506
|
+
`);
|
|
507
|
+
}
|