@principles/pd-cli 1.119.0 → 1.120.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/commands/__tests__/legacy-cleanup.test.d.ts +18 -0
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +1 -0
- package/dist/commands/__tests__/legacy-cleanup.test.js +459 -0
- package/dist/commands/__tests__/legacy-cleanup.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +21 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +179 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-handler.test.d.ts +16 -0
- package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +1 -0
- package/dist/commands/__tests__/rulecode-handler.test.js +285 -0
- package/dist/commands/__tests__/rulecode-handler.test.js.map +1 -0
- package/dist/commands/legacy-cleanup.d.ts +72 -6
- package/dist/commands/legacy-cleanup.d.ts.map +1 -1
- package/dist/commands/legacy-cleanup.js +243 -23
- package/dist/commands/legacy-cleanup.js.map +1 -1
- package/dist/commands/rulecode.d.ts +85 -0
- package/dist/commands/rulecode.d.ts.map +1 -0
- package/dist/commands/rulecode.js +356 -0
- package/dist/commands/rulecode.js.map +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +4 -7
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/index.js +30 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/llm-dogfood.ts +8 -12
- package/src/commands/__tests__/legacy-cleanup.test.ts +596 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +230 -0
- package/src/commands/__tests__/rulecode-handler.test.ts +369 -0
- package/src/commands/legacy-cleanup.ts +335 -27
- package/src/commands/rulecode.ts +434 -0
- package/src/commands/runtime-internalization-run-rulehost.ts +3 -8
- package/src/index.ts +31 -9
- package/tests/commands/cli-command-tree.test.ts +40 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd rulecode — RuleCode dialect spec, static validation, and sandbox replay.
|
|
3
|
+
*
|
|
4
|
+
* PRI-439 Phase 5: Read-only CLI commands that mirror 3 of the 4 Artificer L2
|
|
5
|
+
* agent tools (read_rulecode_spec, validate_rulecode, replay_rulecode). These
|
|
6
|
+
* commands let operators inspect the RuleCode contract and dry-run code
|
|
7
|
+
* against the same pure-logic validators the Artificer L2 agent uses.
|
|
8
|
+
*
|
|
9
|
+
* Subcommands:
|
|
10
|
+
* - spec : print the RuleCode dialect spec text
|
|
11
|
+
* - validate : run static validation (forbidden patterns + return shape +
|
|
12
|
+
* matched=false decision check) on a code string
|
|
13
|
+
* - replay : run sandbox replay of code against a golden trace JSON file
|
|
14
|
+
*
|
|
15
|
+
* All three are READ-ONLY: no DB mutation, no artifact writes, no approvals,
|
|
16
|
+
* no activations. Failure paths include structured reason + nextAction
|
|
17
|
+
* (CLI Operator Gate rule 6).
|
|
18
|
+
*
|
|
19
|
+
* JSON mode is strict: --json outputs exactly one parseable JSON object on
|
|
20
|
+
* stdout, no banners (CLI Operator Gate rule 1).
|
|
21
|
+
*
|
|
22
|
+
* ERR refs:
|
|
23
|
+
* - ERR-001 (no any): all types explicit; untrusted JSON parsed as unknown
|
|
24
|
+
* - ERR-005 (no as bypass): golden trace cases validated with type guards
|
|
25
|
+
* - ERR-009 (fail loud): missing --code/--code-file fails with reason
|
|
26
|
+
* - ERR-002 (graceful degradation with reason): all failure paths include
|
|
27
|
+
* reason + nextAction
|
|
28
|
+
* - ERR-014 (bounded preview): safeStringifyPreview on unknown payloads
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import type { Command } from 'commander';
|
|
34
|
+
import {
|
|
35
|
+
RULECODE_SPEC_TEXT,
|
|
36
|
+
checkForbiddenPatterns,
|
|
37
|
+
checkReturnStatementsMissingFields,
|
|
38
|
+
checkMatchedFalseDecisions,
|
|
39
|
+
evaluateRefinerRuleHostGate,
|
|
40
|
+
buildGoldenTraceFromArtificer,
|
|
41
|
+
createProductionGateDeps,
|
|
42
|
+
} from '@principles/core/runtime-v2';
|
|
43
|
+
import type { GoldenTraceCaseInput } from '@principles/core/runtime-v2';
|
|
44
|
+
import { emitResult } from '../services/cli-output.js';
|
|
45
|
+
|
|
46
|
+
// ── Output types ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export interface RulecodeSpecOutput {
|
|
49
|
+
status: 'ok';
|
|
50
|
+
spec: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RulecodeValidateOutput {
|
|
54
|
+
status: 'ok' | 'failed';
|
|
55
|
+
valid: boolean;
|
|
56
|
+
violationCount: number;
|
|
57
|
+
violations: string[];
|
|
58
|
+
reason?: string;
|
|
59
|
+
nextAction?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RulecodeReplayOutput {
|
|
63
|
+
status: 'ok' | 'failed';
|
|
64
|
+
decision: string;
|
|
65
|
+
reasons: string[];
|
|
66
|
+
failedCases: { caseId: string; errorType: string; message: string }[];
|
|
67
|
+
forbiddenPatternViolations: string[];
|
|
68
|
+
reason?: string;
|
|
69
|
+
nextAction?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve code from --code (inline) or --code-file (path). Fails loud if
|
|
76
|
+
* neither is provided or the file cannot be read.
|
|
77
|
+
*/
|
|
78
|
+
function resolveCode(opts: { code?: string; codeFile?: string }): { code?: string; error?: { reason: string; nextAction: string } } {
|
|
79
|
+
if (opts.code && opts.code.trim().length > 0) {
|
|
80
|
+
return { code: opts.code };
|
|
81
|
+
}
|
|
82
|
+
if (opts.codeFile) {
|
|
83
|
+
try {
|
|
84
|
+
const resolved = path.resolve(opts.codeFile);
|
|
85
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
86
|
+
if (content.trim().length === 0) {
|
|
87
|
+
return { error: { reason: `code file is empty: ${resolved}`, nextAction: 'provide a non-empty rule implementation file' } };
|
|
88
|
+
}
|
|
89
|
+
return { code: content };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
92
|
+
return { error: { reason: `cannot read --code-file: ${reason}`, nextAction: 'verify the file path exists and is readable' } };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { error: { reason: 'no code provided: pass --code <string> or --code-file <path>', nextAction: 'specify one of --code or --code-file' } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Type guard: validate that an unknown value is a GoldenTraceCaseInput.
|
|
100
|
+
* Treats parsed JSON as untrusted (Runtime Contract Rule 1/2/4).
|
|
101
|
+
*/
|
|
102
|
+
function isGoldenTraceCaseInput(value: unknown): value is GoldenTraceCaseInput {
|
|
103
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
104
|
+
const v = value as Record<string, unknown>;
|
|
105
|
+
if (typeof v.caseId !== 'string' || v.caseId.length === 0) return false;
|
|
106
|
+
if (v.kind !== 'positive' && v.kind !== 'negative') return false;
|
|
107
|
+
if (typeof v.toolName !== 'string' || v.toolName.length === 0) return false;
|
|
108
|
+
if (typeof v.params !== 'object' || v.params === null) return false;
|
|
109
|
+
if (typeof v.expectedDecision !== 'string') return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load and validate golden trace cases from a JSON file.
|
|
115
|
+
* Returns either the validated cases or a structured error.
|
|
116
|
+
*/
|
|
117
|
+
function loadGoldenTraceCases(filePath: string): { cases?: GoldenTraceCaseInput[]; error?: { reason: string; nextAction: string } } {
|
|
118
|
+
let raw: string;
|
|
119
|
+
try {
|
|
120
|
+
const resolved = path.resolve(filePath);
|
|
121
|
+
raw = fs.readFileSync(resolved, 'utf8');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
124
|
+
return { error: { reason: `cannot read --golden-trace file: ${reason}`, nextAction: 'verify the file path exists and is readable' } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let parsed: unknown;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(raw);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
132
|
+
return { error: { reason: `golden trace file is not valid JSON: ${reason}`, nextAction: 'fix the JSON syntax in the golden trace file' } };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!Array.isArray(parsed)) {
|
|
136
|
+
return { error: { reason: 'golden trace file must contain a JSON array of cases', nextAction: 'provide an array of GoldenTraceCaseInput objects' } };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate each element (Runtime Contract Rule 4 — validate array element types)
|
|
140
|
+
const cases: GoldenTraceCaseInput[] = [];
|
|
141
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
142
|
+
const element = parsed[i];
|
|
143
|
+
if (!isGoldenTraceCaseInput(element)) {
|
|
144
|
+
return { error: { reason: `golden trace case at index ${i} is malformed (requires caseId, kind, toolName, params, expectedDecision)`, nextAction: `fix case at index ${i} in the golden trace file` } };
|
|
145
|
+
}
|
|
146
|
+
cases.push(element);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (cases.length < 2) {
|
|
150
|
+
return { error: { reason: `golden trace must contain at least 2 cases (1 positive + 1 negative), got ${cases.length}`, nextAction: 'add more cases to the golden trace file' } };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { cases };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
interface SpecOptions {
|
|
159
|
+
workspace?: string;
|
|
160
|
+
json?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function handleRulecodeSpec(opts: SpecOptions): Promise<void> {
|
|
164
|
+
const output: RulecodeSpecOutput = {
|
|
165
|
+
status: 'ok',
|
|
166
|
+
spec: RULECODE_SPEC_TEXT,
|
|
167
|
+
};
|
|
168
|
+
emitResult(output, {
|
|
169
|
+
json: opts.json ?? false,
|
|
170
|
+
formatText: (o) => o.spec,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface ValidateOptions {
|
|
175
|
+
code?: string;
|
|
176
|
+
codeFile?: string;
|
|
177
|
+
workspace?: string;
|
|
178
|
+
json?: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function handleRulecodeValidate(opts: ValidateOptions): Promise<void> {
|
|
182
|
+
const json = opts.json ?? false;
|
|
183
|
+
const codeResult = resolveCode(opts);
|
|
184
|
+
if (codeResult.error || codeResult.code === undefined) {
|
|
185
|
+
const { error } = codeResult;
|
|
186
|
+
const output: RulecodeValidateOutput = {
|
|
187
|
+
status: 'failed',
|
|
188
|
+
valid: false,
|
|
189
|
+
violationCount: 0,
|
|
190
|
+
violations: [],
|
|
191
|
+
reason: error?.reason ?? 'unknown error',
|
|
192
|
+
nextAction: error?.nextAction ?? 'check input and retry',
|
|
193
|
+
};
|
|
194
|
+
emitResult(output, {
|
|
195
|
+
json,
|
|
196
|
+
formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
|
|
197
|
+
});
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { code } = codeResult;
|
|
203
|
+
const forbidden = checkForbiddenPatterns(code);
|
|
204
|
+
const missingFields = checkReturnStatementsMissingFields(code);
|
|
205
|
+
const matchedFalseViolations = checkMatchedFalseDecisions(code);
|
|
206
|
+
|
|
207
|
+
const allViolations = [
|
|
208
|
+
...forbidden.map((label) => `forbidden pattern: ${label}`),
|
|
209
|
+
...missingFields,
|
|
210
|
+
...matchedFalseViolations,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const output: RulecodeValidateOutput = {
|
|
214
|
+
status: allViolations.length === 0 ? 'ok' : 'failed',
|
|
215
|
+
valid: allViolations.length === 0,
|
|
216
|
+
violationCount: allViolations.length,
|
|
217
|
+
violations: allViolations,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (allViolations.length > 0) {
|
|
221
|
+
output.reason = `${allViolations.length} static violation(s) found`;
|
|
222
|
+
output.nextAction = 'fix the listed violations, then run `pd rulecode replay` to verify sandbox behavior';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
emitResult(output, {
|
|
226
|
+
json,
|
|
227
|
+
formatText: (o) => {
|
|
228
|
+
if (o.valid) return 'VALID: no static violations detected.';
|
|
229
|
+
const lines = [`INVALID: ${o.violationCount} violation(s):`];
|
|
230
|
+
for (const v of o.violations) lines.push(` - ${v}`);
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (output.status === 'failed') {
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface ReplayOptions {
|
|
241
|
+
code?: string;
|
|
242
|
+
codeFile?: string;
|
|
243
|
+
goldenTrace?: string;
|
|
244
|
+
workspace?: string;
|
|
245
|
+
json?: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function handleRulecodeReplay(opts: ReplayOptions): Promise<void> {
|
|
249
|
+
const json = opts.json ?? false;
|
|
250
|
+
const codeResult = resolveCode(opts);
|
|
251
|
+
if (codeResult.error || codeResult.code === undefined) {
|
|
252
|
+
const { error } = codeResult;
|
|
253
|
+
const output: RulecodeReplayOutput = {
|
|
254
|
+
status: 'failed',
|
|
255
|
+
decision: 'rejected_input',
|
|
256
|
+
reasons: [],
|
|
257
|
+
failedCases: [],
|
|
258
|
+
forbiddenPatternViolations: [],
|
|
259
|
+
reason: error?.reason ?? 'unknown error',
|
|
260
|
+
nextAction: error?.nextAction ?? 'check input and retry',
|
|
261
|
+
};
|
|
262
|
+
emitResult(output, {
|
|
263
|
+
json,
|
|
264
|
+
formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
|
|
265
|
+
});
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!opts.goldenTrace) {
|
|
271
|
+
// Should not happen — --golden-trace is requiredOption. Defense-in-depth.
|
|
272
|
+
const output: RulecodeReplayOutput = {
|
|
273
|
+
status: 'failed',
|
|
274
|
+
decision: 'rejected_input',
|
|
275
|
+
reasons: [],
|
|
276
|
+
failedCases: [],
|
|
277
|
+
forbiddenPatternViolations: [],
|
|
278
|
+
reason: '--golden-trace is required',
|
|
279
|
+
nextAction: 'pass --golden-trace <path> with a JSON file of golden trace cases',
|
|
280
|
+
};
|
|
281
|
+
emitResult(output, {
|
|
282
|
+
json,
|
|
283
|
+
formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
|
|
284
|
+
});
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const traceResult = loadGoldenTraceCases(opts.goldenTrace);
|
|
290
|
+
if (traceResult.error || traceResult.cases === undefined) {
|
|
291
|
+
const { error } = traceResult;
|
|
292
|
+
const output: RulecodeReplayOutput = {
|
|
293
|
+
status: 'failed',
|
|
294
|
+
decision: 'rejected_input',
|
|
295
|
+
reasons: [],
|
|
296
|
+
failedCases: [],
|
|
297
|
+
forbiddenPatternViolations: [],
|
|
298
|
+
reason: error?.reason ?? 'unknown error',
|
|
299
|
+
nextAction: error?.nextAction ?? 'check golden trace file and retry',
|
|
300
|
+
};
|
|
301
|
+
emitResult(output, {
|
|
302
|
+
json,
|
|
303
|
+
formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
|
|
304
|
+
});
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Build the GoldenTrace from the artificer-input shape.
|
|
310
|
+
const buildResult = buildGoldenTraceFromArtificer({
|
|
311
|
+
cases: traceResult.cases,
|
|
312
|
+
sourceArtifactId: undefined,
|
|
313
|
+
});
|
|
314
|
+
if (!buildResult.ok) {
|
|
315
|
+
const output: RulecodeReplayOutput = {
|
|
316
|
+
status: 'failed',
|
|
317
|
+
decision: 'rejected_input',
|
|
318
|
+
reasons: [],
|
|
319
|
+
failedCases: [],
|
|
320
|
+
forbiddenPatternViolations: [],
|
|
321
|
+
reason: `golden trace build failed: ${buildResult.reason}`,
|
|
322
|
+
nextAction: 'ensure the golden trace has at least 1 positive + 1 negative case',
|
|
323
|
+
};
|
|
324
|
+
emitResult(output, {
|
|
325
|
+
json,
|
|
326
|
+
formatText: (o) => `Error: ${o.reason}\n→ ${o.nextAction}`,
|
|
327
|
+
});
|
|
328
|
+
process.exitCode = 1;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Run the sandbox replay via production gate deps.
|
|
333
|
+
const gateDeps = createProductionGateDeps();
|
|
334
|
+
const gateResult = evaluateRefinerRuleHostGate(
|
|
335
|
+
{ code: codeResult.code, goldenTrace: buildResult.trace },
|
|
336
|
+
gateDeps,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const output: RulecodeReplayOutput = {
|
|
340
|
+
status: gateResult.decision === 'accepted_shadow' ? 'ok' : 'failed',
|
|
341
|
+
decision: gateResult.decision,
|
|
342
|
+
reasons: gateResult.reasons,
|
|
343
|
+
failedCases: gateResult.sandboxResult.failedCases.map((c) => ({
|
|
344
|
+
caseId: c.caseId,
|
|
345
|
+
errorType: c.errorType,
|
|
346
|
+
message: c.message,
|
|
347
|
+
})),
|
|
348
|
+
forbiddenPatternViolations: gateResult.sandboxResult.forbiddenPatternViolations,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (output.status === 'failed') {
|
|
352
|
+
output.reason = `sandbox replay rejected: ${gateResult.decision}`;
|
|
353
|
+
output.nextAction = 'fix the code or golden trace cases based on the failed cases above';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
emitResult(output, {
|
|
357
|
+
json,
|
|
358
|
+
formatText: (o) => {
|
|
359
|
+
if (o.status === 'ok') return 'PASSED: all golden trace cases replayed successfully.';
|
|
360
|
+
const lines = [`FAILED: ${o.decision}`];
|
|
361
|
+
for (const r of o.reasons) lines.push(` - ${r}`);
|
|
362
|
+
for (const c of o.failedCases) lines.push(` - caseId: ${c.caseId} | ${c.errorType}: ${c.message}`);
|
|
363
|
+
for (const p of o.forbiddenPatternViolations) lines.push(` - forbidden: ${p}`);
|
|
364
|
+
return lines.join('\n');
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (output.status === 'failed') {
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Registration ─────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Register the `rulecode` parent command with 3 subcommands (spec, validate,
|
|
377
|
+
* replay). Returns the parent command for chaining.
|
|
378
|
+
*
|
|
379
|
+
* This is the single source of truth for flag registration — both index.ts
|
|
380
|
+
* and parser tests call this function (CLI gate rule 7).
|
|
381
|
+
*/
|
|
382
|
+
export function registerRulecodeCommand(parentCmd: Command): Command {
|
|
383
|
+
const rulecodeCmd = parentCmd
|
|
384
|
+
.command('rulecode')
|
|
385
|
+
.description('RuleCode dialect spec, static validation, and sandbox replay (read-only)');
|
|
386
|
+
|
|
387
|
+
// spec — no code input needed
|
|
388
|
+
rulecodeCmd
|
|
389
|
+
.command('spec')
|
|
390
|
+
.description('Print the RuleCode dialect spec text (canonical form, forbidden patterns, return shape)')
|
|
391
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
392
|
+
.option('--json', 'Output raw JSON')
|
|
393
|
+
.action(async (opts) => {
|
|
394
|
+
await handleRulecodeSpec({ workspace: opts.workspace, json: opts.json });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// validate — static validation only
|
|
398
|
+
rulecodeCmd
|
|
399
|
+
.command('validate')
|
|
400
|
+
.description('Run static validation on rule implementation code (forbidden patterns + return shape)')
|
|
401
|
+
.option('--code <string>', 'Rule implementation source code (inline)')
|
|
402
|
+
.option('--code-file <path>', 'Read rule implementation code from file')
|
|
403
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
404
|
+
.option('--json', 'Output raw JSON')
|
|
405
|
+
.action(async (opts) => {
|
|
406
|
+
await handleRulecodeValidate({
|
|
407
|
+
code: opts.code,
|
|
408
|
+
codeFile: opts.codeFile,
|
|
409
|
+
workspace: opts.workspace,
|
|
410
|
+
json: opts.json,
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// replay — sandbox replay against a golden trace
|
|
415
|
+
rulecodeCmd
|
|
416
|
+
.command('replay')
|
|
417
|
+
.description('Run sandbox replay of rule code against a golden trace JSON file')
|
|
418
|
+
.option('--code <string>', 'Rule implementation source code (inline)')
|
|
419
|
+
.option('--code-file <path>', 'Read rule implementation code from file')
|
|
420
|
+
.requiredOption('--golden-trace <path>', 'JSON file containing golden trace cases array')
|
|
421
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
422
|
+
.option('--json', 'Output raw JSON')
|
|
423
|
+
.action(async (opts) => {
|
|
424
|
+
await handleRulecodeReplay({
|
|
425
|
+
code: opts.code,
|
|
426
|
+
codeFile: opts.codeFile,
|
|
427
|
+
goldenTrace: opts.goldenTrace,
|
|
428
|
+
workspace: opts.workspace,
|
|
429
|
+
json: opts.json,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return rulecodeCmd;
|
|
434
|
+
}
|
|
@@ -31,7 +31,6 @@ import { createSandboxGateDeps } from '../services/rulehost-pipeline-runner.js';
|
|
|
31
31
|
import {
|
|
32
32
|
PiAiRuntimeAdapter,
|
|
33
33
|
ArtificerL2Adapter,
|
|
34
|
-
buildArtificerL2GenerateCode,
|
|
35
34
|
DefaultArtificerValidator,
|
|
36
35
|
resolveAgentRuntimeBinding,
|
|
37
36
|
computeFeatureFlagsFromConfig,
|
|
@@ -184,18 +183,14 @@ function resolveRunRuleHostRuntime(
|
|
|
184
183
|
agentRuntimeProfiles.artificer = artificerBinding.profileId;
|
|
185
184
|
agentRuntimeProfiles.evaluator = evaluator.profileId;
|
|
186
185
|
|
|
187
|
-
const
|
|
186
|
+
const artificerAdapter = new ArtificerL2Adapter({
|
|
188
187
|
provider: artificerProfile.provider,
|
|
189
188
|
model: artificerProfile.model,
|
|
190
|
-
|
|
189
|
+
apiKeyEnv: artificerProfile.apiKeyEnv,
|
|
191
190
|
baseUrl: artificerProfile.baseUrl,
|
|
192
|
-
timeoutMs,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const artificerAdapter = new ArtificerL2Adapter({
|
|
196
|
-
generateCode,
|
|
197
191
|
gateDeps: createSandboxGateDeps(),
|
|
198
192
|
validator: new DefaultArtificerValidator(),
|
|
193
|
+
totalBudgetMs: timeoutMs,
|
|
199
194
|
});
|
|
200
195
|
|
|
201
196
|
return {
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { handleDemoStoryA } from './commands/demo-story-a.js';
|
|
|
53
53
|
import { handleRuntimeFeaturesStatus } from './commands/runtime-features.js';
|
|
54
54
|
import { handleConfigDoctor } from './commands/config-doctor.js';
|
|
55
55
|
import { registerMvpCommands } from './commands/mvp-smoke.js';
|
|
56
|
+
import { registerRulecodeCommand } from './commands/rulecode.js';
|
|
56
57
|
|
|
57
58
|
import { createRequire } from 'module';
|
|
58
59
|
const require = createRequire(import.meta.url);
|
|
@@ -895,23 +896,44 @@ artifactCmd
|
|
|
895
896
|
|
|
896
897
|
const _legacyCleanupCmd = legacyCmd
|
|
897
898
|
.command('cleanup')
|
|
898
|
-
.description('Clean legacy empathy/diagnostician artifacts from workspace')
|
|
899
|
+
.description('Clean legacy empathy/diagnostician artifacts and V1 Artificer artifacts from workspace')
|
|
899
900
|
.requiredOption('-w, --workspace <path>', 'Workspace directory')
|
|
900
|
-
.option('--dry-run', 'Show what would be cleaned without applying
|
|
901
|
-
.option('--apply', 'Actually apply the cleanup'
|
|
902
|
-
.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
901
|
+
.option('--dry-run', 'Show what would be cleaned without applying (default)')
|
|
902
|
+
.option('--apply', 'Actually apply the cleanup')
|
|
903
|
+
.option('--json', 'Output raw JSON')
|
|
904
|
+
.action(async (opts) => {
|
|
905
|
+
// CLI gate rule 4: --dry-run and --apply are mutually exclusive
|
|
906
|
+
if (opts.dryRun && opts.apply) {
|
|
907
|
+
const msg = 'Error: --dry-run and --apply are mutually exclusive';
|
|
908
|
+
if (opts.json) {
|
|
909
|
+
console.log(JSON.stringify({ status: 'failed', reason: msg, nextAction: 'Specify either --dry-run or --apply, not both' }, null, 2));
|
|
910
|
+
} else {
|
|
911
|
+
console.error(msg);
|
|
912
|
+
}
|
|
913
|
+
process.exitCode = 1;
|
|
914
|
+
return;
|
|
907
915
|
}
|
|
908
|
-
|
|
916
|
+
// Default to dry-run if neither flag is set (CLI gate rule 4).
|
|
917
|
+
// Pass undefined through — the handler's logic
|
|
918
|
+
// (opts.apply === true ? false : opts.dryRun !== false) correctly
|
|
919
|
+
// defaults to dry-run when both are undefined.
|
|
920
|
+
await handleLegacyCleanup({
|
|
921
|
+
workspacePath: opts.workspace,
|
|
922
|
+
dryRun: opts.dryRun,
|
|
923
|
+
apply: opts.apply,
|
|
924
|
+
json: opts.json ?? false,
|
|
925
|
+
});
|
|
909
926
|
});
|
|
910
927
|
|
|
911
928
|
// ─── MVP Smoke (PRI-397) ────────────────────────────────────────────────────
|
|
912
929
|
|
|
913
930
|
registerMvpCommands(program);
|
|
914
931
|
|
|
932
|
+
// ─── RuleCode CLI (PRI-439 Phase 5) ─────────────────────────────────────────
|
|
933
|
+
// Read-only commands: spec, validate, replay. No DB mutation, no artifact writes.
|
|
934
|
+
|
|
935
|
+
registerRulecodeCommand(program);
|
|
936
|
+
|
|
915
937
|
const consoleCmd = program
|
|
916
938
|
.command('console')
|
|
917
939
|
.description('Start the pd-console web UI for principle review (default: legacy launcher)')
|
|
@@ -76,4 +76,44 @@ describe('CLI command tree structure', () => {
|
|
|
76
76
|
const output = runPdHelp(['runtime', 'activation', '--help']);
|
|
77
77
|
expect(output).toMatch(/edit\s/);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it('rulecode command exists with spec/validate/replay subcommands (pd rulecode --help)', () => {
|
|
81
|
+
const output = runPdHelp(['rulecode', '--help']);
|
|
82
|
+
expect(output).toContain('spec');
|
|
83
|
+
expect(output).toContain('validate');
|
|
84
|
+
expect(output).toContain('replay');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rulecode spec subcommand has --json and --workspace (pd rulecode spec --help)', () => {
|
|
88
|
+
const output = runPdHelp(['rulecode', 'spec', '--help']);
|
|
89
|
+
expect(output).toContain('--json');
|
|
90
|
+
expect(output).toContain('--workspace');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('rulecode validate subcommand has --code, --code-file, --json (pd rulecode validate --help)', () => {
|
|
94
|
+
const output = runPdHelp(['rulecode', 'validate', '--help']);
|
|
95
|
+
expect(output).toContain('--code');
|
|
96
|
+
expect(output).toContain('--code-file');
|
|
97
|
+
expect(output).toContain('--json');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rulecode replay subcommand has --golden-trace (required), --code, --json (pd rulecode replay --help)', () => {
|
|
101
|
+
const output = runPdHelp(['rulecode', 'replay', '--help']);
|
|
102
|
+
expect(output).toContain('--golden-trace');
|
|
103
|
+
expect(output).toContain('--code');
|
|
104
|
+
expect(output).toContain('--json');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('legacy cleanup subcommand has --dry-run, --apply, --json (pd legacy cleanup --help)', () => {
|
|
108
|
+
const output = runPdHelp(['legacy', 'cleanup', '--help']);
|
|
109
|
+
expect(output).toContain('--dry-run');
|
|
110
|
+
expect(output).toContain('--apply');
|
|
111
|
+
expect(output).toContain('--json');
|
|
112
|
+
expect(output).toContain('--workspace');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('legacy cleanup description mentions V1 Artificer artifacts', () => {
|
|
116
|
+
const output = runPdHelp(['legacy', 'cleanup', '--help']);
|
|
117
|
+
expect(output).toContain('V1 Artificer');
|
|
118
|
+
});
|
|
79
119
|
});
|