@crediolabs/policy-builder-cli 0.1.10 → 0.1.12
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.
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// [--xdr <b64>] [--json] [--quiet] [--out <path>]
|
|
7
7
|
// policy-builder synthesize --mandate <path.json>
|
|
8
8
|
// [--oz-config <path.json>] [--confidence <0..1>]
|
|
9
|
-
// [--json] [--quiet] [--out <path>]
|
|
9
|
+
// [--explain] [--json] [--quiet] [--out <path>]
|
|
10
10
|
// policy-builder synthesize --recorded-tx <path.json> --network <mainnet|testnet>
|
|
11
11
|
// [--responses <path.json>]
|
|
12
12
|
// [--oz-config <path.json>] [--confidence <0..1>]
|
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
// [--window-seconds <n>] [--valid-until <ledger>]
|
|
16
16
|
// [--limit-amount <i128str>] [--invocation-limit <n>]
|
|
17
17
|
// [--recipient <C...|G...>]...
|
|
18
|
-
// [--json] [--quiet] [--out <path>]
|
|
18
|
+
// [--explain] [--json] [--quiet] [--out <path>]
|
|
19
|
+
//
|
|
20
|
+
// --explain (Phase 1) makes the synthesised policy human-readable: the
|
|
21
|
+
// output gains `review` (the deterministic review-card summary) and
|
|
22
|
+
// `predicateTree` (the in-memory interpreter predicate as JSON). Without
|
|
23
|
+
// --explain the output is byte-identical to today.
|
|
19
24
|
//
|
|
20
25
|
// The router is hand-rolled (no commander / yargs dep). It splits argv into
|
|
21
26
|
// `command + subcommand-flags + global-flags` and dispatches to the matching
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
// Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
|
|
15
15
|
// the interpreter opt-in and are rejected without --smart-account; tighten-only
|
|
16
16
|
// bounds are validated by the core.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// --explain (Phase 1) makes the synthesised policy human-readable. The
|
|
19
|
+
// orchestrator attaches the in-memory predicate tree + a SimulationResult
|
|
20
|
+
// to the success envelope; the CLI builds the deterministic review card
|
|
21
|
+
// from those inputs and emits it alongside the policy. Without --explain
|
|
22
|
+
// the output is byte-identical to today.
|
|
23
|
+
import { buildReviewCardSummary, isStellarAddress, } from '@crediolabs/policy-synth';
|
|
18
24
|
import { runSynthesizePolicy } from '@crediolabs/policy-synth/run';
|
|
19
25
|
import { CliError, formatToolResponse, parsePairs, readJsonFile } from "../output.js";
|
|
20
26
|
// Positive-int flags and i128 amount strings share the same wire shape: a
|
|
@@ -33,6 +39,12 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
33
39
|
retryable: false,
|
|
34
40
|
});
|
|
35
41
|
}
|
|
42
|
+
// --explain is a STANDALONE flag (no value). Detect presence via
|
|
43
|
+
// `!== undefined` so `--explain` (no value) and a missing flag are
|
|
44
|
+
// distinguishable. The core defaults to ADDITIVE: with --explain the
|
|
45
|
+
// output gains `review` + `predicateTree` and remains unchanged
|
|
46
|
+
// otherwise.
|
|
47
|
+
const explain = pairs.explain !== undefined;
|
|
36
48
|
if (hasMandate) {
|
|
37
49
|
const mandate = readJsonFile(pairs.mandate);
|
|
38
50
|
const args = { source: 'mandate', mandate };
|
|
@@ -42,7 +54,11 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
42
54
|
if (pairs.confidence !== undefined) {
|
|
43
55
|
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
44
56
|
}
|
|
57
|
+
if (explain)
|
|
58
|
+
args.explain = true;
|
|
45
59
|
const res = await runSynthesizePolicy(args);
|
|
60
|
+
if (explain)
|
|
61
|
+
emitExplainBlock(res, flags);
|
|
46
62
|
return formatToolResponse(res, flags, 'synthesize(mandate)');
|
|
47
63
|
}
|
|
48
64
|
// hasRecorded
|
|
@@ -200,9 +216,60 @@ export async function runSynthesizeCommand(argv, flags) {
|
|
|
200
216
|
}
|
|
201
217
|
args.interpreter = interpreter;
|
|
202
218
|
}
|
|
219
|
+
if (explain)
|
|
220
|
+
args.explain = true;
|
|
203
221
|
const res = await runSynthesizePolicy(args);
|
|
222
|
+
// formatToolResponse runs FIRST so the "ok" line + --out / --json writes
|
|
223
|
+
// happen with the additive fields already attached. emitExplainBlock
|
|
224
|
+
// mutates `res.data` in place to inject `review` + `predicateTree`, so
|
|
225
|
+
// the --out artefact and --json stdout both carry the same shape.
|
|
226
|
+
if (explain)
|
|
227
|
+
emitExplainBlock(res, flags);
|
|
204
228
|
return formatToolResponse(res, flags, 'synthesize(recording)');
|
|
205
229
|
}
|
|
230
|
+
/** Augment the tool response envelope with the --explain fields and
|
|
231
|
+
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
232
|
+
* single seam that places `review` + `predicateTree` on the wire
|
|
233
|
+
* envelope; the core stays downstream of `formatToolResponse` so the
|
|
234
|
+
* byte-identical no-flag path is preserved. The envelope mutation is
|
|
235
|
+
* done BEFORE `formatToolResponse` so the on-disk --out artefact and
|
|
236
|
+
* the --json stdout both carry the same fields. */
|
|
237
|
+
function emitExplainBlock(res, flags) {
|
|
238
|
+
if (!res.ok || !res.explain)
|
|
239
|
+
return;
|
|
240
|
+
const review = buildReviewCardSummary(
|
|
241
|
+
// The orchestrator's `predicateTree` is the exact in-memory AST
|
|
242
|
+
// (canonical JSON shape). The builder's input is typed as
|
|
243
|
+
// `PredicateNode | null`; the orchestrator's `null` is the truthful
|
|
244
|
+
// OZ-only / mandate value, so a null here is honest.
|
|
245
|
+
(res.explain.predicateTree ?? null), res.data?.policyRefs ?? [], res.data?.contextRule ?? {
|
|
246
|
+
contextRuleType: { kind: 'default' },
|
|
247
|
+
name: 'unknown',
|
|
248
|
+
validUntilLedger: null,
|
|
249
|
+
signers: [],
|
|
250
|
+
policies: [],
|
|
251
|
+
}, res.explain.simulation);
|
|
252
|
+
res.data.review =
|
|
253
|
+
review;
|
|
254
|
+
res.data.predicateTree = res.explain.predicateTree;
|
|
255
|
+
// In human mode (no --json), print the review card readably: the rule name,
|
|
256
|
+
// the expiry, then one line per constraint. Plain text; no colour, no box
|
|
257
|
+
// drawing. The "ok" line is printed by formatToolResponse; this block prints
|
|
258
|
+
// ONLY the review-card lines.
|
|
259
|
+
//
|
|
260
|
+
// `review.plainEnglish` is deliberately not printed here: it is the same
|
|
261
|
+
// constraint list joined into one sentence, so emitting both would state
|
|
262
|
+
// every constraint twice. It stays on the JSON envelope for callers that
|
|
263
|
+
// want a single-string summary.
|
|
264
|
+
if (!flags.json && !flags.quiet) {
|
|
265
|
+
process.stdout.write(`Review card: ${review.ruleName}\n`);
|
|
266
|
+
process.stdout.write(` ${review.expiry}\n`);
|
|
267
|
+
process.stdout.write(' Constraints:\n');
|
|
268
|
+
for (const line of review.constraints) {
|
|
269
|
+
process.stdout.write(` - ${line}\n`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
206
273
|
/** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
|
|
207
274
|
* CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
|
|
208
275
|
* on `ozConfig` catches shape mismatches downstream. */
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
// Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
|
|
16
16
|
// the interpreter opt-in and are rejected without --smart-account; tighten-only
|
|
17
17
|
// bounds are validated by the core.
|
|
18
|
+
//
|
|
19
|
+
// --explain (Phase 1) makes the synthesised policy human-readable. The
|
|
20
|
+
// orchestrator attaches the in-memory predicate tree + a SimulationResult
|
|
21
|
+
// to the success envelope; the CLI builds the deterministic review card
|
|
22
|
+
// from those inputs and emits it alongside the policy. Without --explain
|
|
23
|
+
// the output is byte-identical to today.
|
|
18
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
25
|
exports.runSynthesizeCommand = runSynthesizeCommand;
|
|
20
26
|
const policy_synth_1 = require("@crediolabs/policy-synth");
|
|
@@ -36,6 +42,12 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
36
42
|
retryable: false,
|
|
37
43
|
});
|
|
38
44
|
}
|
|
45
|
+
// --explain is a STANDALONE flag (no value). Detect presence via
|
|
46
|
+
// `!== undefined` so `--explain` (no value) and a missing flag are
|
|
47
|
+
// distinguishable. The core defaults to ADDITIVE: with --explain the
|
|
48
|
+
// output gains `review` + `predicateTree` and remains unchanged
|
|
49
|
+
// otherwise.
|
|
50
|
+
const explain = pairs.explain !== undefined;
|
|
39
51
|
if (hasMandate) {
|
|
40
52
|
const mandate = (0, output_ts_1.readJsonFile)(pairs.mandate);
|
|
41
53
|
const args = { source: 'mandate', mandate };
|
|
@@ -45,7 +57,11 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
45
57
|
if (pairs.confidence !== undefined) {
|
|
46
58
|
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence) };
|
|
47
59
|
}
|
|
60
|
+
if (explain)
|
|
61
|
+
args.explain = true;
|
|
48
62
|
const res = await (0, run_1.runSynthesizePolicy)(args);
|
|
63
|
+
if (explain)
|
|
64
|
+
emitExplainBlock(res, flags);
|
|
49
65
|
return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(mandate)');
|
|
50
66
|
}
|
|
51
67
|
// hasRecorded
|
|
@@ -203,9 +219,60 @@ async function runSynthesizeCommand(argv, flags) {
|
|
|
203
219
|
}
|
|
204
220
|
args.interpreter = interpreter;
|
|
205
221
|
}
|
|
222
|
+
if (explain)
|
|
223
|
+
args.explain = true;
|
|
206
224
|
const res = await (0, run_1.runSynthesizePolicy)(args);
|
|
225
|
+
// formatToolResponse runs FIRST so the "ok" line + --out / --json writes
|
|
226
|
+
// happen with the additive fields already attached. emitExplainBlock
|
|
227
|
+
// mutates `res.data` in place to inject `review` + `predicateTree`, so
|
|
228
|
+
// the --out artefact and --json stdout both carry the same shape.
|
|
229
|
+
if (explain)
|
|
230
|
+
emitExplainBlock(res, flags);
|
|
207
231
|
return (0, output_ts_1.formatToolResponse)(res, flags, 'synthesize(recording)');
|
|
208
232
|
}
|
|
233
|
+
/** Augment the tool response envelope with the --explain fields and
|
|
234
|
+
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
235
|
+
* single seam that places `review` + `predicateTree` on the wire
|
|
236
|
+
* envelope; the core stays downstream of `formatToolResponse` so the
|
|
237
|
+
* byte-identical no-flag path is preserved. The envelope mutation is
|
|
238
|
+
* done BEFORE `formatToolResponse` so the on-disk --out artefact and
|
|
239
|
+
* the --json stdout both carry the same fields. */
|
|
240
|
+
function emitExplainBlock(res, flags) {
|
|
241
|
+
if (!res.ok || !res.explain)
|
|
242
|
+
return;
|
|
243
|
+
const review = (0, policy_synth_1.buildReviewCardSummary)(
|
|
244
|
+
// The orchestrator's `predicateTree` is the exact in-memory AST
|
|
245
|
+
// (canonical JSON shape). The builder's input is typed as
|
|
246
|
+
// `PredicateNode | null`; the orchestrator's `null` is the truthful
|
|
247
|
+
// OZ-only / mandate value, so a null here is honest.
|
|
248
|
+
(res.explain.predicateTree ?? null), res.data?.policyRefs ?? [], res.data?.contextRule ?? {
|
|
249
|
+
contextRuleType: { kind: 'default' },
|
|
250
|
+
name: 'unknown',
|
|
251
|
+
validUntilLedger: null,
|
|
252
|
+
signers: [],
|
|
253
|
+
policies: [],
|
|
254
|
+
}, res.explain.simulation);
|
|
255
|
+
res.data.review =
|
|
256
|
+
review;
|
|
257
|
+
res.data.predicateTree = res.explain.predicateTree;
|
|
258
|
+
// In human mode (no --json), print the review card readably: the rule name,
|
|
259
|
+
// the expiry, then one line per constraint. Plain text; no colour, no box
|
|
260
|
+
// drawing. The "ok" line is printed by formatToolResponse; this block prints
|
|
261
|
+
// ONLY the review-card lines.
|
|
262
|
+
//
|
|
263
|
+
// `review.plainEnglish` is deliberately not printed here: it is the same
|
|
264
|
+
// constraint list joined into one sentence, so emitting both would state
|
|
265
|
+
// every constraint twice. It stays on the JSON envelope for callers that
|
|
266
|
+
// want a single-string summary.
|
|
267
|
+
if (!flags.json && !flags.quiet) {
|
|
268
|
+
process.stdout.write(`Review card: ${review.ruleName}\n`);
|
|
269
|
+
process.stdout.write(` ${review.expiry}\n`);
|
|
270
|
+
process.stdout.write(' Constraints:\n');
|
|
271
|
+
for (const line of review.constraints) {
|
|
272
|
+
process.stdout.write(` - ${line}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
209
276
|
/** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
|
|
210
277
|
* CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
|
|
211
278
|
* on `ozConfig` catches shape mismatches downstream. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-builder-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI wrapper around the OZ policy-synth core (record + synthesize) for solo-dev and CI workflows.",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"prepack": "bun run build"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@crediolabs/policy-synth": "0.1.
|
|
66
|
+
"@crediolabs/policy-synth": "0.1.10",
|
|
67
67
|
"zod": "3.25.76"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
@@ -14,11 +14,33 @@
|
|
|
14
14
|
// Oracle params (--oracle-max-staleness, --oracle-max-deviation) are part of
|
|
15
15
|
// the interpreter opt-in and are rejected without --smart-account; tighten-only
|
|
16
16
|
// bounds are validated by the core.
|
|
17
|
+
//
|
|
18
|
+
// --explain (Phase 1) makes the synthesised policy human-readable. The
|
|
19
|
+
// orchestrator attaches the in-memory predicate tree + a SimulationResult
|
|
20
|
+
// to the success envelope; the CLI builds the deterministic review card
|
|
21
|
+
// from those inputs and emits it alongside the policy. Without --explain
|
|
22
|
+
// the output is byte-identical to today.
|
|
17
23
|
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
buildReviewCardSummary,
|
|
26
|
+
isStellarAddress,
|
|
27
|
+
type ProposedPolicy,
|
|
28
|
+
type ReviewCardSummary,
|
|
29
|
+
} from '@crediolabs/policy-synth'
|
|
19
30
|
import { runSynthesizePolicy } from '@crediolabs/policy-synth/run'
|
|
20
31
|
import { CliError, type CliFlags, formatToolResponse, parsePairs, readJsonFile } from '../output.ts'
|
|
21
32
|
|
|
33
|
+
/** Local mirror of the synth core's `SimulationResult` shape. The verify
|
|
34
|
+
* module is internal to the package, so the CLI keeps this minimal copy
|
|
35
|
+
* instead of importing the deep path - the contract is small enough that
|
|
36
|
+
* a structural type is cheaper than a new package export. */
|
|
37
|
+
type SimulationResult = {
|
|
38
|
+
permit: { tx: 'permit' } | { tx: 'deny'; reason: string }
|
|
39
|
+
evaluatedCases: Array<{ dimension: string; outcome: 'permit' | 'deny'; reason: string }>
|
|
40
|
+
backend: 'interpreter-v1' | 'ts-model'
|
|
41
|
+
simulatorVersion: string
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
// Positive-int flags and i128 amount strings share the same wire shape: a
|
|
23
45
|
// base-10 unsigned decimal, no sign. The i128 stays a string at the boundary
|
|
24
46
|
// because it is wider than Number.MAX_SAFE_INTEGER.
|
|
@@ -40,6 +62,13 @@ export async function runSynthesizeCommand(
|
|
|
40
62
|
})
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
// --explain is a STANDALONE flag (no value). Detect presence via
|
|
66
|
+
// `!== undefined` so `--explain` (no value) and a missing flag are
|
|
67
|
+
// distinguishable. The core defaults to ADDITIVE: with --explain the
|
|
68
|
+
// output gains `review` + `predicateTree` and remains unchanged
|
|
69
|
+
// otherwise.
|
|
70
|
+
const explain = pairs.explain !== undefined
|
|
71
|
+
|
|
43
72
|
if (hasMandate) {
|
|
44
73
|
const mandate = readJsonFile(pairs.mandate as string) as Record<string, unknown>
|
|
45
74
|
const args: Record<string, unknown> = { source: 'mandate', mandate }
|
|
@@ -49,7 +78,9 @@ export async function runSynthesizeCommand(
|
|
|
49
78
|
if (pairs.confidence !== undefined) {
|
|
50
79
|
args.confidenceOverride = { threshold: parseConfidence(pairs.confidence as string) }
|
|
51
80
|
}
|
|
81
|
+
if (explain) args.explain = true
|
|
52
82
|
const res = await runSynthesizePolicy(args)
|
|
83
|
+
if (explain) emitExplainBlock(res, flags)
|
|
53
84
|
return formatToolResponse(res, flags, 'synthesize(mandate)')
|
|
54
85
|
}
|
|
55
86
|
|
|
@@ -230,10 +261,78 @@ export async function runSynthesizeCommand(
|
|
|
230
261
|
}
|
|
231
262
|
args.interpreter = interpreter
|
|
232
263
|
}
|
|
264
|
+
if (explain) args.explain = true
|
|
233
265
|
const res = await runSynthesizePolicy(args)
|
|
266
|
+
// formatToolResponse runs FIRST so the "ok" line + --out / --json writes
|
|
267
|
+
// happen with the additive fields already attached. emitExplainBlock
|
|
268
|
+
// mutates `res.data` in place to inject `review` + `predicateTree`, so
|
|
269
|
+
// the --out artefact and --json stdout both carry the same shape.
|
|
270
|
+
if (explain) emitExplainBlock(res, flags)
|
|
234
271
|
return formatToolResponse(res, flags, 'synthesize(recording)')
|
|
235
272
|
}
|
|
236
273
|
|
|
274
|
+
/** Augment the tool response envelope with the --explain fields and
|
|
275
|
+
* (in non-JSON mode) print the review card readably. The CLI is the
|
|
276
|
+
* single seam that places `review` + `predicateTree` on the wire
|
|
277
|
+
* envelope; the core stays downstream of `formatToolResponse` so the
|
|
278
|
+
* byte-identical no-flag path is preserved. The envelope mutation is
|
|
279
|
+
* done BEFORE `formatToolResponse` so the on-disk --out artefact and
|
|
280
|
+
* the --json stdout both carry the same fields. */
|
|
281
|
+
function emitExplainBlock(
|
|
282
|
+
res: {
|
|
283
|
+
ok: boolean
|
|
284
|
+
data?: ProposedPolicy
|
|
285
|
+
explain?: {
|
|
286
|
+
predicateTree: unknown
|
|
287
|
+
simulation: SimulationResult
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
flags: CliFlags
|
|
291
|
+
): void {
|
|
292
|
+
if (!res.ok || !res.explain) return
|
|
293
|
+
const review = buildReviewCardSummary(
|
|
294
|
+
// The orchestrator's `predicateTree` is the exact in-memory AST
|
|
295
|
+
// (canonical JSON shape). The builder's input is typed as
|
|
296
|
+
// `PredicateNode | null`; the orchestrator's `null` is the truthful
|
|
297
|
+
// OZ-only / mandate value, so a null here is honest.
|
|
298
|
+
(res.explain.predicateTree ?? null) as never,
|
|
299
|
+
res.data?.policyRefs ?? [],
|
|
300
|
+
res.data?.contextRule ?? {
|
|
301
|
+
contextRuleType: { kind: 'default' as const },
|
|
302
|
+
name: 'unknown',
|
|
303
|
+
validUntilLedger: null,
|
|
304
|
+
signers: [],
|
|
305
|
+
policies: [],
|
|
306
|
+
},
|
|
307
|
+
res.explain.simulation
|
|
308
|
+
)
|
|
309
|
+
// Attach the two additive fields to the on-wire envelope. formatToolResponse
|
|
310
|
+
// reads from `res` and writes the JSON; we mutate the same object so the
|
|
311
|
+
// --json path + --out path see the same shape.
|
|
312
|
+
;(res.data as ProposedPolicy & { review: ReviewCardSummary; predicateTree: unknown }).review =
|
|
313
|
+
review
|
|
314
|
+
;(
|
|
315
|
+
res.data as ProposedPolicy & { review: ReviewCardSummary; predicateTree: unknown }
|
|
316
|
+
).predicateTree = res.explain.predicateTree
|
|
317
|
+
// In human mode (no --json), print the review card readably: the rule name,
|
|
318
|
+
// the expiry, then one line per constraint. Plain text; no colour, no box
|
|
319
|
+
// drawing. The "ok" line is printed by formatToolResponse; this block prints
|
|
320
|
+
// ONLY the review-card lines.
|
|
321
|
+
//
|
|
322
|
+
// `review.plainEnglish` is deliberately not printed here: it is the same
|
|
323
|
+
// constraint list joined into one sentence, so emitting both would state
|
|
324
|
+
// every constraint twice. It stays on the JSON envelope for callers that
|
|
325
|
+
// want a single-string summary.
|
|
326
|
+
if (!flags.json && !flags.quiet) {
|
|
327
|
+
process.stdout.write(`Review card: ${review.ruleName}\n`)
|
|
328
|
+
process.stdout.write(` ${review.expiry}\n`)
|
|
329
|
+
process.stdout.write(' Constraints:\n')
|
|
330
|
+
for (const line of review.constraints) {
|
|
331
|
+
process.stdout.write(` - ${line}\n`)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
237
336
|
/** Read and validate an OzAdapterConfig JSON file. Throws CLI_FILE_NOT_FOUND /
|
|
238
337
|
* CLI_INVALID_JSON for filesystem / parse failures; the core's strict schema
|
|
239
338
|
* on `ozConfig` catches shape mismatches downstream. */
|