my-frontend-observer 0.3.0 → 0.5.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/CHANGELOG.md +112 -0
- package/README.md +59 -7
- package/dist/application/comparisonService.d.ts +56 -0
- package/dist/application/comparisonService.js +77 -0
- package/dist/application/comparisonService.js.map +1 -0
- package/dist/application/frontendContractEvaluationService.d.ts +49 -0
- package/dist/application/frontendContractEvaluationService.js +112 -0
- package/dist/application/frontendContractEvaluationService.js.map +1 -0
- package/dist/application/frontendContractPersistenceService.d.ts +56 -0
- package/dist/application/frontendContractPersistenceService.js +91 -0
- package/dist/application/frontendContractPersistenceService.js.map +1 -0
- package/dist/artifacts/artifactReader.d.ts +19 -0
- package/dist/artifacts/artifactReader.js +36 -0
- package/dist/artifacts/artifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactReader.d.ts +18 -0
- package/dist/artifacts/comparisonArtifactReader.js +35 -0
- package/dist/artifacts/comparisonArtifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
- package/dist/artifacts/comparisonArtifactWriter.js +67 -0
- package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
- package/dist/artifacts/frontendContractArtifactReader.d.ts +24 -0
- package/dist/artifacts/frontendContractArtifactReader.js +47 -0
- package/dist/artifacts/frontendContractArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractArtifactWriter.d.ts +34 -0
- package/dist/artifacts/frontendContractArtifactWriter.js +70 -0
- package/dist/artifacts/frontendContractArtifactWriter.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.d.ts +17 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js +34 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.d.ts +32 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js +58 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js.map +1 -0
- package/dist/cli.js +712 -3
- package/dist/cli.js.map +1 -1
- package/dist/domain/comparison.d.ts +198 -0
- package/dist/domain/comparison.js +324 -0
- package/dist/domain/comparison.js.map +1 -0
- package/dist/domain/comparisonEngine.d.ts +48 -0
- package/dist/domain/comparisonEngine.js +694 -0
- package/dist/domain/comparisonEngine.js.map +1 -0
- package/dist/domain/comparisonIdentity.d.ts +13 -0
- package/dist/domain/comparisonIdentity.js +46 -0
- package/dist/domain/comparisonIdentity.js.map +1 -0
- package/dist/domain/evidence.d.ts +2 -0
- package/dist/domain/evidence.js +4 -0
- package/dist/domain/evidence.js.map +1 -1
- package/dist/domain/frontendContractEvaluation.d.ts +57 -0
- package/dist/domain/frontendContractEvaluation.js +454 -0
- package/dist/domain/frontendContractEvaluation.js.map +1 -0
- package/dist/domain/frontendContractEvaluationArtifact.d.ts +65 -0
- package/dist/domain/frontendContractEvaluationArtifact.js +108 -0
- package/dist/domain/frontendContractEvaluationArtifact.js.map +1 -0
- package/dist/domain/frontendContractIdentity.d.ts +39 -0
- package/dist/domain/frontendContractIdentity.js +70 -0
- package/dist/domain/frontendContractIdentity.js.map +1 -0
- package/dist/domain/frontendContracts.d.ts +188 -0
- package/dist/domain/frontendContracts.js +260 -0
- package/dist/domain/frontendContracts.js.map +1 -0
- package/dist/domain/relationships.d.ts +168 -0
- package/dist/domain/relationships.js +343 -0
- package/dist/domain/relationships.js.map +1 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +173 -3
- package/docs/CI_CD.md +76 -0
- package/docs/COMMANDS.md +299 -0
- package/docs/CONTRACTS.md +270 -6
- package/docs/CURRENT_STATE.md +183 -8
- package/docs/DEVELOPMENT.md +83 -12
- package/docs/PROJECT_OVERVIEW.md +18 -8
- package/docs/QUICKSTART.md +10 -0
- package/docs/RELEASE.md +17 -8
- package/docs/ROADMAP.md +10 -0
- package/docs/SECURITY.md +26 -1
- package/docs/WORKFLOWS.md +113 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { normalizeRequest } from './request/request.js';
|
|
5
5
|
import { getProducerInfo } from './domain/schema.js';
|
|
6
6
|
import { observe } from './application/observationPersistence.js';
|
|
7
|
+
import { compareAndPersistFromArtifactRoots } from './application/comparisonService.js';
|
|
8
|
+
import { approveAndPersistBaseline, persistPerChangeContract } from './application/frontendContractPersistenceService.js';
|
|
9
|
+
import { evaluateAndPersistFromArtifactRoots } from './application/frontendContractEvaluationService.js';
|
|
7
10
|
const defaultIO = {
|
|
8
11
|
stdout: (text) => {
|
|
9
12
|
process.stdout.write(text);
|
|
@@ -18,14 +21,25 @@ Usage:
|
|
|
18
21
|
my-frontend-observer <command> [options]
|
|
19
22
|
|
|
20
23
|
Commands:
|
|
21
|
-
observe
|
|
22
|
-
|
|
24
|
+
observe Capture one bounded browser observation and persist
|
|
25
|
+
it as a portable artifact.
|
|
26
|
+
compare Compare two persisted observations and write a
|
|
27
|
+
structured comparison artifact.
|
|
28
|
+
approve-baseline Explicitly approve and persist one already-authored
|
|
29
|
+
persistent baseline contract against the observation
|
|
30
|
+
it claims to approve.
|
|
31
|
+
save-change-contract Validate and persist one already-authored per-change
|
|
32
|
+
contract so it can later be evaluated.
|
|
33
|
+
evaluate-contract Evaluate a candidate change against an approved
|
|
34
|
+
baseline, a per-change contract, and existing
|
|
35
|
+
before/after/comparison evidence, and persist the
|
|
36
|
+
result.
|
|
23
37
|
|
|
24
38
|
Options:
|
|
25
39
|
--help Show this help.
|
|
26
40
|
--version Print the package version.
|
|
27
41
|
|
|
28
|
-
Run "my-frontend-observer
|
|
42
|
+
Run "my-frontend-observer <command> --help" for command-specific options.
|
|
29
43
|
`;
|
|
30
44
|
const OBSERVE_HELP = `Usage:
|
|
31
45
|
my-frontend-observer observe --url <loopback-url> [options]
|
|
@@ -73,6 +87,143 @@ request, unsafe/failed navigation, or a failed artifact write, prints
|
|
|
73
87
|
structured diagnostics to stderr and exits nonzero. No progress output is
|
|
74
88
|
printed during a normal capture.
|
|
75
89
|
`;
|
|
90
|
+
const COMPARE_HELP = `Usage:
|
|
91
|
+
my-frontend-observer compare --before <observation-artifact-root> --after <observation-artifact-root> --output <directory> [options]
|
|
92
|
+
|
|
93
|
+
Required:
|
|
94
|
+
--before <path> Root directory of the "before" persisted
|
|
95
|
+
observation artifact (the directory containing
|
|
96
|
+
its manifest.json).
|
|
97
|
+
--after <path> Root directory of the "after" persisted
|
|
98
|
+
observation artifact.
|
|
99
|
+
--output <directory> Portable, relative output location for the
|
|
100
|
+
comparison artifact.
|
|
101
|
+
|
|
102
|
+
Options:
|
|
103
|
+
--config-file <json-file> Loads a comparison configuration from a local
|
|
104
|
+
JSON file: { "geometryTolerancePx": <0-10>,
|
|
105
|
+
"expectedDependencies": [ { "cause": { "target":
|
|
106
|
+
"...", "property": "x"|"y"|"width"|"height",
|
|
107
|
+
"direction": "increase"|"decrease"|"change"|
|
|
108
|
+
"unchanged" }, "effect": { ... same shape ... },
|
|
109
|
+
"source": "explicit-config" } ] }. Relative
|
|
110
|
+
paths resolve from the current working
|
|
111
|
+
directory; the file path itself is never
|
|
112
|
+
persisted into the artifact or included in the
|
|
113
|
+
comparison request identity. Without
|
|
114
|
+
--config-file, geometryTolerancePx defaults to
|
|
115
|
+
0.5 with no declared dependencies.
|
|
116
|
+
--help Show this help.
|
|
117
|
+
|
|
118
|
+
Comparison reads two already-persisted observation artifacts and derives
|
|
119
|
+
evidence purely from their existing content - it never launches a browser
|
|
120
|
+
and never re-observes either target. On success, prints a concise result
|
|
121
|
+
and exits 0, including when the two observations are found to be
|
|
122
|
+
"incomparable" (that is itself a successful comparison outcome, not a
|
|
123
|
+
failure). On invalid syntax, an unreadable or structurally invalid source
|
|
124
|
+
artifact, invalid configuration, or a failed artifact write, prints
|
|
125
|
+
structured diagnostics to stderr and exits nonzero. No progress output is
|
|
126
|
+
printed during a normal comparison.
|
|
127
|
+
`;
|
|
128
|
+
const APPROVE_BASELINE_HELP = `Usage:
|
|
129
|
+
my-frontend-observer approve-baseline --observation <observation-artifact-root> --contract-file <json-file> --output <directory>
|
|
130
|
+
|
|
131
|
+
Required:
|
|
132
|
+
--observation <path> Root directory of the persisted observation
|
|
133
|
+
artifact (the directory containing its
|
|
134
|
+
manifest.json) that this baseline claims to
|
|
135
|
+
approve.
|
|
136
|
+
--contract-file <json-file> Local JSON file containing one already-authored
|
|
137
|
+
persistent baseline contract (the raw contract
|
|
138
|
+
value, no wrapper field). Relative paths resolve
|
|
139
|
+
from the current working directory; the file
|
|
140
|
+
path itself is never persisted or included in
|
|
141
|
+
any identity.
|
|
142
|
+
--output <directory> Portable, relative output location for the
|
|
143
|
+
baseline artifact.
|
|
144
|
+
|
|
145
|
+
Options:
|
|
146
|
+
--help Show this help.
|
|
147
|
+
|
|
148
|
+
This command is the only explicit baseline-approval act in the observer -
|
|
149
|
+
approval is never inferred from a successful comparison or evaluation. The
|
|
150
|
+
supplied contract's source-observation reference must match the supplied
|
|
151
|
+
observation artifact's stable identity; a mismatched or unrelated observation
|
|
152
|
+
is rejected. Any \`supersedesBaselineId\` already authored in the contract is
|
|
153
|
+
preserved exactly - this command never discovers, infers, or deletes a prior
|
|
154
|
+
baseline. Remains local and non-mutating: it never launches a browser and
|
|
155
|
+
never modifies the source observation or any existing baseline artifact. On
|
|
156
|
+
success, prints a concise result and exits 0. On invalid syntax, an
|
|
157
|
+
unreadable/malformed contract file, a non-baseline contract, a
|
|
158
|
+
structurally invalid contract, a source-observation mismatch, an existing
|
|
159
|
+
artifact collision, or a persistence failure, prints structured diagnostics
|
|
160
|
+
to stderr and exits nonzero.
|
|
161
|
+
`;
|
|
162
|
+
const SAVE_CHANGE_CONTRACT_HELP = `Usage:
|
|
163
|
+
my-frontend-observer save-change-contract --contract-file <json-file> --output <directory>
|
|
164
|
+
|
|
165
|
+
Required:
|
|
166
|
+
--contract-file <json-file> Local JSON file containing one already-authored
|
|
167
|
+
per-change contract (the raw contract value, no
|
|
168
|
+
wrapper field). Relative paths resolve from the
|
|
169
|
+
current working directory; the file path itself
|
|
170
|
+
is never persisted or included in any identity.
|
|
171
|
+
--output <directory> Portable, relative output location for the
|
|
172
|
+
change-contract artifact.
|
|
173
|
+
|
|
174
|
+
Options:
|
|
175
|
+
--help Show this help.
|
|
176
|
+
|
|
177
|
+
This command validates and persists a per-change contract only - it does not
|
|
178
|
+
approve anything. Any \`supersedesBaselineClauseIds\` already authored on a
|
|
179
|
+
clause is preserved exactly; resolving those references against a particular
|
|
180
|
+
baseline remains \`evaluate-contract\`'s responsibility, not this command's.
|
|
181
|
+
Remains local and non-mutating. On success, prints a concise result and
|
|
182
|
+
exits 0. On invalid syntax, an unreadable/malformed contract file, a
|
|
183
|
+
non-change contract (e.g. a persistent baseline contract), a structurally
|
|
184
|
+
invalid contract (including an authored \`unexpected\` category, which is
|
|
185
|
+
never a valid authored scope), an existing artifact collision, or a
|
|
186
|
+
persistence failure, prints structured diagnostics to stderr and exits
|
|
187
|
+
nonzero.
|
|
188
|
+
`;
|
|
189
|
+
const EVALUATE_CONTRACT_HELP = `Usage:
|
|
190
|
+
my-frontend-observer evaluate-contract --before <observation-artifact-root> --after <observation-artifact-root> --comparison <comparison-artifact-root> --baseline <baseline-contract-artifact-root> --change <per-change-contract-artifact-root> --output <directory> [--enforce]
|
|
191
|
+
|
|
192
|
+
Required:
|
|
193
|
+
--before <path> Root directory of the "before" persisted observation
|
|
194
|
+
artifact.
|
|
195
|
+
--after <path> Root directory of the "after" persisted observation
|
|
196
|
+
artifact.
|
|
197
|
+
--comparison <path> Root directory of the already-persisted comparison
|
|
198
|
+
artifact for that before/after pair.
|
|
199
|
+
--baseline <path> Root directory of the already-approved persistent
|
|
200
|
+
baseline contract artifact.
|
|
201
|
+
--change <path> Root directory of the already-persisted per-change
|
|
202
|
+
contract artifact.
|
|
203
|
+
--output <directory> Portable, relative output location for the
|
|
204
|
+
evaluation artifact.
|
|
205
|
+
|
|
206
|
+
Options:
|
|
207
|
+
--enforce Make a FAIL verdict produce a nonzero process exit status. A
|
|
208
|
+
FAIL evaluation is always persisted and printed identically
|
|
209
|
+
with or without this flag - it changes only the process exit
|
|
210
|
+
code, never evaluation identity, contents, or persistence.
|
|
211
|
+
--help Show this help.
|
|
212
|
+
|
|
213
|
+
This command never launches a browser, never re-resolves targets, and never
|
|
214
|
+
recomputes comparison or relationship evidence - it reads the already-
|
|
215
|
+
persisted before/after observations and comparison exactly as given and
|
|
216
|
+
evaluates the supplied baseline/change contracts against them exactly once.
|
|
217
|
+
A FAIL verdict (a found regression or unsatisfied contract clause) is a
|
|
218
|
+
successful, persisted evaluation outcome, not an execution error; without
|
|
219
|
+
--enforce it exits 0 like PASS. On success (evaluation constructed and
|
|
220
|
+
persisted, verdict PASS, or verdict FAIL without --enforce), prints a
|
|
221
|
+
concise result and exits 0. With --enforce and verdict FAIL, prints the same
|
|
222
|
+
result and exits nonzero. On invalid syntax, an unreadable/malformed/
|
|
223
|
+
incoherent source artifact, or a persistence failure (evaluation could not
|
|
224
|
+
even be constructed), prints structured diagnostics to stderr, persists
|
|
225
|
+
nothing, and exits nonzero.
|
|
226
|
+
`;
|
|
76
227
|
function parseViewport(raw) {
|
|
77
228
|
const match = /^(\d+)x(\d+)$/.exec(raw);
|
|
78
229
|
if (!match)
|
|
@@ -269,6 +420,384 @@ function loadScrollScenarioFile(filePath) {
|
|
|
269
420
|
}
|
|
270
421
|
return { ok: true, scenario: parsed };
|
|
271
422
|
}
|
|
423
|
+
/** CLI-syntax-only parsing, mirroring `parseObserveArgs`: shape/presence/duplication errors only. Comparison-config semantics stay owned by the existing domain validator. */
|
|
424
|
+
function parseCompareArgs(argv) {
|
|
425
|
+
const errors = [];
|
|
426
|
+
let beforeRoot;
|
|
427
|
+
let beforeFlagCount = 0;
|
|
428
|
+
let afterRoot;
|
|
429
|
+
let afterFlagCount = 0;
|
|
430
|
+
let outputLocation;
|
|
431
|
+
let outputFlagCount = 0;
|
|
432
|
+
let configFilePath;
|
|
433
|
+
let configFileFlagCount = 0;
|
|
434
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
435
|
+
const arg = argv[i];
|
|
436
|
+
switch (arg) {
|
|
437
|
+
case '--before': {
|
|
438
|
+
const value = argv[(i += 1)];
|
|
439
|
+
beforeFlagCount += 1;
|
|
440
|
+
if (value === undefined) {
|
|
441
|
+
errors.push('--before requires a path argument');
|
|
442
|
+
}
|
|
443
|
+
else if (beforeFlagCount > 1) {
|
|
444
|
+
errors.push('--before may only be specified once');
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
beforeRoot = value;
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case '--after': {
|
|
452
|
+
const value = argv[(i += 1)];
|
|
453
|
+
afterFlagCount += 1;
|
|
454
|
+
if (value === undefined) {
|
|
455
|
+
errors.push('--after requires a path argument');
|
|
456
|
+
}
|
|
457
|
+
else if (afterFlagCount > 1) {
|
|
458
|
+
errors.push('--after may only be specified once');
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
afterRoot = value;
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case '--output': {
|
|
466
|
+
const value = argv[(i += 1)];
|
|
467
|
+
outputFlagCount += 1;
|
|
468
|
+
if (value === undefined) {
|
|
469
|
+
errors.push('--output requires a directory argument');
|
|
470
|
+
}
|
|
471
|
+
else if (outputFlagCount > 1) {
|
|
472
|
+
errors.push('--output may only be specified once');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
outputLocation = value;
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case '--config-file': {
|
|
480
|
+
const value = argv[(i += 1)];
|
|
481
|
+
configFileFlagCount += 1;
|
|
482
|
+
if (value === undefined) {
|
|
483
|
+
errors.push('--config-file requires a file path argument');
|
|
484
|
+
}
|
|
485
|
+
else if (configFileFlagCount > 1) {
|
|
486
|
+
errors.push('--config-file may only be specified once');
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
configFilePath = value;
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (beforeRoot === undefined)
|
|
498
|
+
errors.push('--before is required');
|
|
499
|
+
if (afterRoot === undefined)
|
|
500
|
+
errors.push('--after is required');
|
|
501
|
+
if (outputLocation === undefined)
|
|
502
|
+
errors.push('--output is required');
|
|
503
|
+
if (errors.length > 0)
|
|
504
|
+
return { ok: false, errors };
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
beforeRoot: beforeRoot,
|
|
508
|
+
afterRoot: afterRoot,
|
|
509
|
+
outputLocation: outputLocation,
|
|
510
|
+
...(configFilePath === undefined ? {} : { configFilePath }),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* CLI/input-boundary-only responsibility, mirroring `loadScrollScenarioFile`:
|
|
515
|
+
* read one local JSON file and validate only the root shape this file format
|
|
516
|
+
* owns (plain, non-array object) - the file supplies the value of
|
|
517
|
+
* `ComparisonConfig` directly (no wrapper field). Every semantic rule
|
|
518
|
+
* (geometry tolerance bounds, dependency property/direction vocabulary,
|
|
519
|
+
* dependency source/provenance) stays owned by the existing comparison
|
|
520
|
+
* domain validator (`isValidComparisonConfig`, invoked inside
|
|
521
|
+
* `compareObservations`), not duplicated here. The file path itself is
|
|
522
|
+
* never returned to the caller beyond this function, so it can never reach
|
|
523
|
+
* the persisted comparison artifact or its request identity.
|
|
524
|
+
*/
|
|
525
|
+
function loadComparisonConfigFile(filePath) {
|
|
526
|
+
let rawText;
|
|
527
|
+
try {
|
|
528
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
532
|
+
return { ok: false, error: `--config-file could not be read: ${message}` };
|
|
533
|
+
}
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(rawText);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
540
|
+
return { ok: false, error: `--config-file is not valid JSON: ${message}` };
|
|
541
|
+
}
|
|
542
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
543
|
+
return { ok: false, error: '--config-file root must be a JSON object' };
|
|
544
|
+
}
|
|
545
|
+
return { ok: true, config: parsed };
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* CLI/input-boundary-only responsibility, mirroring `loadComparisonConfigFile`:
|
|
549
|
+
* read one local JSON file and validate only the root shape this file format
|
|
550
|
+
* owns (plain, non-array object) - the file supplies the raw contract value
|
|
551
|
+
* directly (no wrapper field). Every semantic/structural rule (artifact
|
|
552
|
+
* kind, schema version, contract class, clause shape, authored category
|
|
553
|
+
* vocabulary) stays owned by the existing frozen domain validators
|
|
554
|
+
* (`isValidPersistentBaselineContract`/`isValidPerChangeContract`, invoked
|
|
555
|
+
* inside the application layer), never duplicated here. The file path
|
|
556
|
+
* itself is never returned to the caller beyond this function, so it can
|
|
557
|
+
* never reach a persisted artifact or its identity.
|
|
558
|
+
*/
|
|
559
|
+
function loadContractFile(filePath, flagLabel) {
|
|
560
|
+
let rawText;
|
|
561
|
+
try {
|
|
562
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
566
|
+
return { ok: false, error: `${flagLabel} could not be read: ${message}` };
|
|
567
|
+
}
|
|
568
|
+
let parsed;
|
|
569
|
+
try {
|
|
570
|
+
parsed = JSON.parse(rawText);
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
574
|
+
return { ok: false, error: `${flagLabel} is not valid JSON: ${message}` };
|
|
575
|
+
}
|
|
576
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
577
|
+
return { ok: false, error: `${flagLabel} root must be a JSON object` };
|
|
578
|
+
}
|
|
579
|
+
return { ok: true, contract: parsed };
|
|
580
|
+
}
|
|
581
|
+
/** CLI-syntax-only parsing, mirroring `parseCompareArgs`. */
|
|
582
|
+
function parseApproveBaselineArgs(argv) {
|
|
583
|
+
const errors = [];
|
|
584
|
+
let observationRoot;
|
|
585
|
+
let observationFlagCount = 0;
|
|
586
|
+
let contractFilePath;
|
|
587
|
+
let contractFileFlagCount = 0;
|
|
588
|
+
let outputLocation;
|
|
589
|
+
let outputFlagCount = 0;
|
|
590
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
591
|
+
const arg = argv[i];
|
|
592
|
+
switch (arg) {
|
|
593
|
+
case '--observation': {
|
|
594
|
+
const value = argv[(i += 1)];
|
|
595
|
+
observationFlagCount += 1;
|
|
596
|
+
if (value === undefined)
|
|
597
|
+
errors.push('--observation requires a path argument');
|
|
598
|
+
else if (observationFlagCount > 1)
|
|
599
|
+
errors.push('--observation may only be specified once');
|
|
600
|
+
else
|
|
601
|
+
observationRoot = value;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case '--contract-file': {
|
|
605
|
+
const value = argv[(i += 1)];
|
|
606
|
+
contractFileFlagCount += 1;
|
|
607
|
+
if (value === undefined)
|
|
608
|
+
errors.push('--contract-file requires a file path argument');
|
|
609
|
+
else if (contractFileFlagCount > 1)
|
|
610
|
+
errors.push('--contract-file may only be specified once');
|
|
611
|
+
else
|
|
612
|
+
contractFilePath = value;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case '--output': {
|
|
616
|
+
const value = argv[(i += 1)];
|
|
617
|
+
outputFlagCount += 1;
|
|
618
|
+
if (value === undefined)
|
|
619
|
+
errors.push('--output requires a directory argument');
|
|
620
|
+
else if (outputFlagCount > 1)
|
|
621
|
+
errors.push('--output may only be specified once');
|
|
622
|
+
else
|
|
623
|
+
outputLocation = value;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
default:
|
|
627
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (observationRoot === undefined)
|
|
631
|
+
errors.push('--observation is required');
|
|
632
|
+
if (contractFilePath === undefined)
|
|
633
|
+
errors.push('--contract-file is required');
|
|
634
|
+
if (outputLocation === undefined)
|
|
635
|
+
errors.push('--output is required');
|
|
636
|
+
if (errors.length > 0)
|
|
637
|
+
return { ok: false, errors };
|
|
638
|
+
return { ok: true, observationRoot: observationRoot, contractFilePath: contractFilePath, outputLocation: outputLocation };
|
|
639
|
+
}
|
|
640
|
+
/** CLI-syntax-only parsing, mirroring `parseCompareArgs`. */
|
|
641
|
+
function parseSaveChangeContractArgs(argv) {
|
|
642
|
+
const errors = [];
|
|
643
|
+
let contractFilePath;
|
|
644
|
+
let contractFileFlagCount = 0;
|
|
645
|
+
let outputLocation;
|
|
646
|
+
let outputFlagCount = 0;
|
|
647
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
648
|
+
const arg = argv[i];
|
|
649
|
+
switch (arg) {
|
|
650
|
+
case '--contract-file': {
|
|
651
|
+
const value = argv[(i += 1)];
|
|
652
|
+
contractFileFlagCount += 1;
|
|
653
|
+
if (value === undefined)
|
|
654
|
+
errors.push('--contract-file requires a file path argument');
|
|
655
|
+
else if (contractFileFlagCount > 1)
|
|
656
|
+
errors.push('--contract-file may only be specified once');
|
|
657
|
+
else
|
|
658
|
+
contractFilePath = value;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case '--output': {
|
|
662
|
+
const value = argv[(i += 1)];
|
|
663
|
+
outputFlagCount += 1;
|
|
664
|
+
if (value === undefined)
|
|
665
|
+
errors.push('--output requires a directory argument');
|
|
666
|
+
else if (outputFlagCount > 1)
|
|
667
|
+
errors.push('--output may only be specified once');
|
|
668
|
+
else
|
|
669
|
+
outputLocation = value;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
default:
|
|
673
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (contractFilePath === undefined)
|
|
677
|
+
errors.push('--contract-file is required');
|
|
678
|
+
if (outputLocation === undefined)
|
|
679
|
+
errors.push('--output is required');
|
|
680
|
+
if (errors.length > 0)
|
|
681
|
+
return { ok: false, errors };
|
|
682
|
+
return { ok: true, contractFilePath: contractFilePath, outputLocation: outputLocation };
|
|
683
|
+
}
|
|
684
|
+
/** CLI-syntax-only parsing. `--enforce` is a boolean switch (no value); repeating it is harmless (idempotent), matching a boolean flag's natural semantics rather than the "may only be specified once" policy used for single-value flags. */
|
|
685
|
+
function parseEvaluateContractArgs(argv) {
|
|
686
|
+
const errors = [];
|
|
687
|
+
let beforeRoot;
|
|
688
|
+
let beforeFlagCount = 0;
|
|
689
|
+
let afterRoot;
|
|
690
|
+
let afterFlagCount = 0;
|
|
691
|
+
let comparisonRoot;
|
|
692
|
+
let comparisonFlagCount = 0;
|
|
693
|
+
let baselineRoot;
|
|
694
|
+
let baselineFlagCount = 0;
|
|
695
|
+
let changeRoot;
|
|
696
|
+
let changeFlagCount = 0;
|
|
697
|
+
let outputLocation;
|
|
698
|
+
let outputFlagCount = 0;
|
|
699
|
+
let enforce = false;
|
|
700
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
701
|
+
const arg = argv[i];
|
|
702
|
+
switch (arg) {
|
|
703
|
+
case '--before': {
|
|
704
|
+
const value = argv[(i += 1)];
|
|
705
|
+
beforeFlagCount += 1;
|
|
706
|
+
if (value === undefined)
|
|
707
|
+
errors.push('--before requires a path argument');
|
|
708
|
+
else if (beforeFlagCount > 1)
|
|
709
|
+
errors.push('--before may only be specified once');
|
|
710
|
+
else
|
|
711
|
+
beforeRoot = value;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case '--after': {
|
|
715
|
+
const value = argv[(i += 1)];
|
|
716
|
+
afterFlagCount += 1;
|
|
717
|
+
if (value === undefined)
|
|
718
|
+
errors.push('--after requires a path argument');
|
|
719
|
+
else if (afterFlagCount > 1)
|
|
720
|
+
errors.push('--after may only be specified once');
|
|
721
|
+
else
|
|
722
|
+
afterRoot = value;
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case '--comparison': {
|
|
726
|
+
const value = argv[(i += 1)];
|
|
727
|
+
comparisonFlagCount += 1;
|
|
728
|
+
if (value === undefined)
|
|
729
|
+
errors.push('--comparison requires a path argument');
|
|
730
|
+
else if (comparisonFlagCount > 1)
|
|
731
|
+
errors.push('--comparison may only be specified once');
|
|
732
|
+
else
|
|
733
|
+
comparisonRoot = value;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case '--baseline': {
|
|
737
|
+
const value = argv[(i += 1)];
|
|
738
|
+
baselineFlagCount += 1;
|
|
739
|
+
if (value === undefined)
|
|
740
|
+
errors.push('--baseline requires a path argument');
|
|
741
|
+
else if (baselineFlagCount > 1)
|
|
742
|
+
errors.push('--baseline may only be specified once');
|
|
743
|
+
else
|
|
744
|
+
baselineRoot = value;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case '--change': {
|
|
748
|
+
const value = argv[(i += 1)];
|
|
749
|
+
changeFlagCount += 1;
|
|
750
|
+
if (value === undefined)
|
|
751
|
+
errors.push('--change requires a path argument');
|
|
752
|
+
else if (changeFlagCount > 1)
|
|
753
|
+
errors.push('--change may only be specified once');
|
|
754
|
+
else
|
|
755
|
+
changeRoot = value;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case '--output': {
|
|
759
|
+
const value = argv[(i += 1)];
|
|
760
|
+
outputFlagCount += 1;
|
|
761
|
+
if (value === undefined)
|
|
762
|
+
errors.push('--output requires a directory argument');
|
|
763
|
+
else if (outputFlagCount > 1)
|
|
764
|
+
errors.push('--output may only be specified once');
|
|
765
|
+
else
|
|
766
|
+
outputLocation = value;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case '--enforce':
|
|
770
|
+
enforce = true;
|
|
771
|
+
break;
|
|
772
|
+
default:
|
|
773
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (beforeRoot === undefined)
|
|
777
|
+
errors.push('--before is required');
|
|
778
|
+
if (afterRoot === undefined)
|
|
779
|
+
errors.push('--after is required');
|
|
780
|
+
if (comparisonRoot === undefined)
|
|
781
|
+
errors.push('--comparison is required');
|
|
782
|
+
if (baselineRoot === undefined)
|
|
783
|
+
errors.push('--baseline is required');
|
|
784
|
+
if (changeRoot === undefined)
|
|
785
|
+
errors.push('--change is required');
|
|
786
|
+
if (outputLocation === undefined)
|
|
787
|
+
errors.push('--output is required');
|
|
788
|
+
if (errors.length > 0)
|
|
789
|
+
return { ok: false, errors };
|
|
790
|
+
return {
|
|
791
|
+
ok: true,
|
|
792
|
+
beforeRoot: beforeRoot,
|
|
793
|
+
afterRoot: afterRoot,
|
|
794
|
+
comparisonRoot: comparisonRoot,
|
|
795
|
+
baselineRoot: baselineRoot,
|
|
796
|
+
changeRoot: changeRoot,
|
|
797
|
+
outputLocation: outputLocation,
|
|
798
|
+
enforce,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
272
801
|
function formatDiagnostic(diagnostic) {
|
|
273
802
|
const target = diagnostic.targetName === undefined ? '' : ` (target: ${diagnostic.targetName})`;
|
|
274
803
|
return `[${diagnostic.code}] ${diagnostic.message}${target}`;
|
|
@@ -326,6 +855,174 @@ async function runObserveCommand(argv, io) {
|
|
|
326
855
|
io.stdout(`Diagnostics: ${result.diagnostics.length}\n`);
|
|
327
856
|
return NON_SUCCESS_COMPLETION_STATES.has(result.completion.state) ? 1 : 0;
|
|
328
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* Thin orchestration only: parse args, optionally load a config file, then
|
|
860
|
+
* delegate to the existing `compareAndPersistFromArtifactRoots` application
|
|
861
|
+
* function exactly once. No comparability/geometry/relationship/dependency
|
|
862
|
+
* logic lives here - see `src/domain/comparisonEngine.ts`. `incomparable` is
|
|
863
|
+
* a successful comparison outcome (the operation determined the two
|
|
864
|
+
* observations should not be treated as equivalent frontend states), so it
|
|
865
|
+
* exits 0 exactly like `comparable`/`comparable-with-warnings`; only a
|
|
866
|
+
* genuine parse/read/domain/persistence failure exits nonzero.
|
|
867
|
+
*/
|
|
868
|
+
async function runCompareCommand(argv, io) {
|
|
869
|
+
if (argv.includes('--help')) {
|
|
870
|
+
io.stdout(COMPARE_HELP);
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
const parsedArgs = parseCompareArgs(argv);
|
|
874
|
+
if (!parsedArgs.ok) {
|
|
875
|
+
for (const error of parsedArgs.errors)
|
|
876
|
+
io.stderr(`error: ${error}\n`);
|
|
877
|
+
io.stderr(COMPARE_HELP);
|
|
878
|
+
return 1;
|
|
879
|
+
}
|
|
880
|
+
let config;
|
|
881
|
+
if (parsedArgs.configFilePath !== undefined) {
|
|
882
|
+
const loaded = loadComparisonConfigFile(parsedArgs.configFilePath);
|
|
883
|
+
if (!loaded.ok) {
|
|
884
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
885
|
+
io.stderr(COMPARE_HELP);
|
|
886
|
+
return 1;
|
|
887
|
+
}
|
|
888
|
+
config = loaded.config;
|
|
889
|
+
}
|
|
890
|
+
// Exactly one application comparison attempt: two artifact reads, one pure comparison, persisted at most once.
|
|
891
|
+
const result = await compareAndPersistFromArtifactRoots(parsedArgs.beforeRoot, parsedArgs.afterRoot, {
|
|
892
|
+
...(config === undefined ? {} : { config }),
|
|
893
|
+
outputLocation: parsedArgs.outputLocation,
|
|
894
|
+
});
|
|
895
|
+
if (!result.ok) {
|
|
896
|
+
for (const diagnostic of result.diagnostics)
|
|
897
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
io.stdout(`Comparison: ${result.comparisonId}\n`);
|
|
901
|
+
io.stdout(`State: ${result.comparability}\n`);
|
|
902
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
903
|
+
io.stdout(`Differences: ${result.differenceCount}\n`);
|
|
904
|
+
io.stdout(`Relationship changes: ${result.relationshipChangeCount}\n`);
|
|
905
|
+
io.stdout(`Diagnostics: ${result.diagnosticsCount}\n`);
|
|
906
|
+
return 0;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Thin orchestration only: parse args, load the raw contract JSON file, then
|
|
910
|
+
* delegate to the existing `approveAndPersistBaseline` application function
|
|
911
|
+
* exactly once. No contract/coherence validation lives here - see
|
|
912
|
+
* `src/application/frontendContractPersistenceService.ts`. This is the only
|
|
913
|
+
* command in the observer that approves a baseline.
|
|
914
|
+
*/
|
|
915
|
+
async function runApproveBaselineCommand(argv, io) {
|
|
916
|
+
if (argv.includes('--help')) {
|
|
917
|
+
io.stdout(APPROVE_BASELINE_HELP);
|
|
918
|
+
return 0;
|
|
919
|
+
}
|
|
920
|
+
const parsedArgs = parseApproveBaselineArgs(argv);
|
|
921
|
+
if (!parsedArgs.ok) {
|
|
922
|
+
for (const error of parsedArgs.errors)
|
|
923
|
+
io.stderr(`error: ${error}\n`);
|
|
924
|
+
io.stderr(APPROVE_BASELINE_HELP);
|
|
925
|
+
return 1;
|
|
926
|
+
}
|
|
927
|
+
const loaded = loadContractFile(parsedArgs.contractFilePath, '--contract-file');
|
|
928
|
+
if (!loaded.ok) {
|
|
929
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
930
|
+
io.stderr(APPROVE_BASELINE_HELP);
|
|
931
|
+
return 1;
|
|
932
|
+
}
|
|
933
|
+
// Exactly one application approval attempt: one observation read, one coherence check, persisted at most once.
|
|
934
|
+
const result = await approveAndPersistBaseline(loaded.contract, parsedArgs.observationRoot, { outputLocation: parsedArgs.outputLocation });
|
|
935
|
+
if (!result.ok) {
|
|
936
|
+
for (const diagnostic of result.diagnostics)
|
|
937
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
938
|
+
return 1;
|
|
939
|
+
}
|
|
940
|
+
io.stdout(`Baseline: ${result.baselineId}\n`);
|
|
941
|
+
io.stdout(`State: approved\n`);
|
|
942
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
943
|
+
io.stdout(`Clauses: ${result.clauseCount}\n`);
|
|
944
|
+
io.stdout(`Supersedes: ${result.supersedesBaselineId ?? 'none'}\n`);
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Thin orchestration only: parse args, load the raw contract JSON file, then
|
|
949
|
+
* delegate to the existing `persistPerChangeContract` application function
|
|
950
|
+
* exactly once. Persistence, not approval.
|
|
951
|
+
*/
|
|
952
|
+
async function runSaveChangeContractCommand(argv, io) {
|
|
953
|
+
if (argv.includes('--help')) {
|
|
954
|
+
io.stdout(SAVE_CHANGE_CONTRACT_HELP);
|
|
955
|
+
return 0;
|
|
956
|
+
}
|
|
957
|
+
const parsedArgs = parseSaveChangeContractArgs(argv);
|
|
958
|
+
if (!parsedArgs.ok) {
|
|
959
|
+
for (const error of parsedArgs.errors)
|
|
960
|
+
io.stderr(`error: ${error}\n`);
|
|
961
|
+
io.stderr(SAVE_CHANGE_CONTRACT_HELP);
|
|
962
|
+
return 1;
|
|
963
|
+
}
|
|
964
|
+
const loaded = loadContractFile(parsedArgs.contractFilePath, '--contract-file');
|
|
965
|
+
if (!loaded.ok) {
|
|
966
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
967
|
+
io.stderr(SAVE_CHANGE_CONTRACT_HELP);
|
|
968
|
+
return 1;
|
|
969
|
+
}
|
|
970
|
+
// Exactly one application persistence attempt.
|
|
971
|
+
const result = await persistPerChangeContract(loaded.contract, { outputLocation: parsedArgs.outputLocation });
|
|
972
|
+
if (!result.ok) {
|
|
973
|
+
for (const diagnostic of result.diagnostics)
|
|
974
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
975
|
+
return 1;
|
|
976
|
+
}
|
|
977
|
+
io.stdout(`Change contract: ${result.contractId}\n`);
|
|
978
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
979
|
+
io.stdout(`Clauses: ${result.clauseCount}\n`);
|
|
980
|
+
io.stdout(`Supersedes baseline clauses: ${result.supersedesBaselineClauseCount}\n`);
|
|
981
|
+
return 0;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Thin orchestration only: parse args, then delegate to the existing
|
|
985
|
+
* `evaluateAndPersistFromArtifactRoots` application function exactly once.
|
|
986
|
+
* No evaluation/tolerance/conflict/unexpected-classification logic lives
|
|
987
|
+
* here - see `src/domain/frontendContractEvaluation.ts`. `--enforce` is
|
|
988
|
+
* applied only after the evaluation has already been constructed and
|
|
989
|
+
* persisted: it selects the process exit status for an already-final FAIL
|
|
990
|
+
* result and never affects evaluation identity, contents, or persistence. A
|
|
991
|
+
* FAIL verdict is a successful, persisted evaluation outcome (a found
|
|
992
|
+
* regression), never treated as a construction/persistence failure.
|
|
993
|
+
*/
|
|
994
|
+
async function runEvaluateContractCommand(argv, io) {
|
|
995
|
+
if (argv.includes('--help')) {
|
|
996
|
+
io.stdout(EVALUATE_CONTRACT_HELP);
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
const parsedArgs = parseEvaluateContractArgs(argv);
|
|
1000
|
+
if (!parsedArgs.ok) {
|
|
1001
|
+
for (const error of parsedArgs.errors)
|
|
1002
|
+
io.stderr(`error: ${error}\n`);
|
|
1003
|
+
io.stderr(EVALUATE_CONTRACT_HELP);
|
|
1004
|
+
return 1;
|
|
1005
|
+
}
|
|
1006
|
+
// Exactly one application evaluation attempt: reads before/after/comparison/baseline/change once each,
|
|
1007
|
+
// calls the canonical evaluator exactly once, and persists exactly one evaluation artifact.
|
|
1008
|
+
const result = await evaluateAndPersistFromArtifactRoots(parsedArgs.beforeRoot, parsedArgs.afterRoot, parsedArgs.comparisonRoot, parsedArgs.baselineRoot, parsedArgs.changeRoot, {
|
|
1009
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1010
|
+
});
|
|
1011
|
+
if (!result.ok) {
|
|
1012
|
+
for (const diagnostic of result.diagnostics)
|
|
1013
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1014
|
+
return 1;
|
|
1015
|
+
}
|
|
1016
|
+
io.stdout(`Evaluation: ${result.evaluationId}\n`);
|
|
1017
|
+
io.stdout(`Verdict: ${result.overallVerdict}\n`);
|
|
1018
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1019
|
+
io.stdout(`Clauses: ${result.clauseResultCount}\n`);
|
|
1020
|
+
io.stdout(`Unexpected: ${result.unexpectedChangeCount}\n`);
|
|
1021
|
+
io.stdout(`Enforced: ${parsedArgs.enforce ? 'yes' : 'no'}\n`);
|
|
1022
|
+
if (parsedArgs.enforce && result.overallVerdict === 'FAIL')
|
|
1023
|
+
return 1;
|
|
1024
|
+
return 0;
|
|
1025
|
+
}
|
|
329
1026
|
/** Testable CLI entry point: pure function of argv (+ injectable IO), no direct process.exit. */
|
|
330
1027
|
export async function runCli(argv, io = defaultIO) {
|
|
331
1028
|
const [command, ...rest] = argv;
|
|
@@ -344,6 +1041,18 @@ export async function runCli(argv, io = defaultIO) {
|
|
|
344
1041
|
if (command === 'observe') {
|
|
345
1042
|
return runObserveCommand(rest, io);
|
|
346
1043
|
}
|
|
1044
|
+
if (command === 'compare') {
|
|
1045
|
+
return runCompareCommand(rest, io);
|
|
1046
|
+
}
|
|
1047
|
+
if (command === 'approve-baseline') {
|
|
1048
|
+
return runApproveBaselineCommand(rest, io);
|
|
1049
|
+
}
|
|
1050
|
+
if (command === 'save-change-contract') {
|
|
1051
|
+
return runSaveChangeContractCommand(rest, io);
|
|
1052
|
+
}
|
|
1053
|
+
if (command === 'evaluate-contract') {
|
|
1054
|
+
return runEvaluateContractCommand(rest, io);
|
|
1055
|
+
}
|
|
347
1056
|
io.stderr(`error: unrecognized command "${command}"\n`);
|
|
348
1057
|
io.stderr(TOP_LEVEL_HELP);
|
|
349
1058
|
return 1;
|