my-frontend-observer 0.6.0 → 0.7.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 +79 -0
- package/README.md +43 -4
- package/dist/application/externalReferencePersistenceService.d.ts +75 -0
- package/dist/application/externalReferencePersistenceService.js +182 -0
- package/dist/application/externalReferencePersistenceService.js.map +1 -0
- package/dist/application/referenceFidelityEvaluationService.d.ts +28 -0
- package/dist/application/referenceFidelityEvaluationService.js +45 -0
- package/dist/application/referenceFidelityEvaluationService.js.map +1 -0
- package/dist/artifacts/externalReferenceArtifactReader.d.ts +18 -0
- package/dist/artifacts/externalReferenceArtifactReader.js +35 -0
- package/dist/artifacts/externalReferenceArtifactReader.js.map +1 -0
- package/dist/artifacts/externalReferenceArtifactWriter.d.ts +44 -0
- package/dist/artifacts/externalReferenceArtifactWriter.js +77 -0
- package/dist/artifacts/externalReferenceArtifactWriter.js.map +1 -0
- package/dist/cli.js +844 -0
- package/dist/cli.js.map +1 -1
- package/dist/domain/boundedAgentContext.d.ts +52 -0
- package/dist/domain/boundedAgentContext.js +44 -1
- package/dist/domain/boundedAgentContext.js.map +1 -1
- package/dist/domain/boundedAgentContextIdentity.d.ts +16 -6
- package/dist/domain/boundedAgentContextIdentity.js +22 -6
- package/dist/domain/boundedAgentContextIdentity.js.map +1 -1
- package/dist/domain/boundedAgentContextProjection.d.ts +11 -0
- package/dist/domain/boundedAgentContextProjection.js +32 -4
- package/dist/domain/boundedAgentContextProjection.js.map +1 -1
- package/dist/domain/comparison.d.ts +23 -1
- package/dist/domain/comparison.js +27 -1
- package/dist/domain/comparison.js.map +1 -1
- package/dist/domain/comparisonEngine.d.ts +34 -6
- package/dist/domain/comparisonEngine.js +48 -8
- package/dist/domain/comparisonEngine.js.map +1 -1
- package/dist/domain/diagnostics.d.ts +1 -1
- package/dist/domain/diagnostics.js +16 -0
- package/dist/domain/diagnostics.js.map +1 -1
- package/dist/domain/explicitState.d.ts +40 -0
- package/dist/domain/explicitState.js +54 -0
- package/dist/domain/explicitState.js.map +1 -0
- package/dist/domain/externalReference.d.ts +158 -0
- package/dist/domain/externalReference.js +167 -0
- package/dist/domain/externalReference.js.map +1 -0
- package/dist/domain/externalReferenceApplicability.d.ts +43 -0
- package/dist/domain/externalReferenceApplicability.js +52 -0
- package/dist/domain/externalReferenceApplicability.js.map +1 -0
- package/dist/domain/externalReferenceCompatibility.d.ts +40 -0
- package/dist/domain/externalReferenceCompatibility.js +48 -0
- package/dist/domain/externalReferenceCompatibility.js.map +1 -0
- package/dist/domain/externalReferenceFidelity.d.ts +141 -0
- package/dist/domain/externalReferenceFidelity.js +413 -0
- package/dist/domain/externalReferenceFidelity.js.map +1 -0
- package/dist/domain/externalReferenceIdentity.d.ts +38 -0
- package/dist/domain/externalReferenceIdentity.js +70 -0
- package/dist/domain/externalReferenceIdentity.js.map +1 -0
- package/dist/domain/externalReferenceImage.d.ts +35 -0
- package/dist/domain/externalReferenceImage.js +160 -0
- package/dist/domain/externalReferenceImage.js.map +1 -0
- package/dist/domain/externalReferenceRegionRelationships.d.ts +63 -0
- package/dist/domain/externalReferenceRegionRelationships.js +98 -0
- package/dist/domain/externalReferenceRegionRelationships.js.map +1 -0
- package/dist/domain/externalReferenceRegions.d.ts +65 -0
- package/dist/domain/externalReferenceRegions.js +105 -0
- package/dist/domain/externalReferenceRegions.js.map +1 -0
- package/dist/domain/externalReferenceRequirementIdentity.d.ts +12 -0
- package/dist/domain/externalReferenceRequirementIdentity.js +35 -0
- package/dist/domain/externalReferenceRequirementIdentity.js.map +1 -0
- package/dist/domain/externalReferenceRequirements.d.ts +215 -0
- package/dist/domain/externalReferenceRequirements.js +401 -0
- package/dist/domain/externalReferenceRequirements.js.map +1 -0
- package/dist/domain/externalReferenceRuntimeBinding.d.ts +146 -0
- package/dist/domain/externalReferenceRuntimeBinding.js +183 -0
- package/dist/domain/externalReferenceRuntimeBinding.js.map +1 -0
- package/dist/domain/identity.d.ts +11 -8
- package/dist/domain/identity.js +12 -8
- package/dist/domain/identity.js.map +1 -1
- package/dist/domain/referenceCorrectionIdentity.d.ts +40 -0
- package/dist/domain/referenceCorrectionIdentity.js +77 -0
- package/dist/domain/referenceCorrectionIdentity.js.map +1 -0
- package/dist/domain/referenceCorrectionWorkflow.d.ts +160 -0
- package/dist/domain/referenceCorrectionWorkflow.js +165 -0
- package/dist/domain/referenceCorrectionWorkflow.js.map +1 -0
- package/dist/domain/referenceFidelityProjection.d.ts +65 -0
- package/dist/domain/referenceFidelityProjection.js +135 -0
- package/dist/domain/referenceFidelityProjection.js.map +1 -0
- package/dist/domain/relationships.d.ts +43 -1
- package/dist/domain/relationships.js +15 -6
- package/dist/domain/relationships.js.map +1 -1
- package/dist/domain/schema.js +6 -0
- package/dist/domain/schema.js.map +1 -1
- package/dist/index.d.ts +39 -4
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/request/request.d.ts +12 -0
- package/dist/request/request.js +12 -0
- package/dist/request/request.js.map +1 -1
- package/docs/ARCHITECTURE.md +271 -11
- package/docs/CI_CD.md +50 -0
- package/docs/COMMANDS.md +187 -0
- package/docs/CONTRACTS.md +1347 -12
- package/docs/CURRENT_STATE.md +451 -13
- package/docs/PROJECT_DESCRIPTION.md +513 -88
- package/docs/PROJECT_MILESTONES.md +550 -97
- package/docs/PROJECT_OVERVIEW.md +56 -14
- package/docs/QUICKSTART.md +6 -0
- package/docs/RELEASE.md +28 -21
- package/docs/ROADMAP.md +238 -99
- package/docs/SECURITY.md +80 -4
- package/docs/WORKFLOWS.md +358 -23
- package/docs/reports/v0.7-bounded-fidelity-context-prompt7.md +243 -0
- package/docs/reports/v0.7-implementation-completeness-documentation-reconciliation.md +497 -0
- package/docs/reports/v0.7-pre-release-readiness.md +337 -0
- package/docs/reports/v0.7-reference-binding-prompt5.md +223 -0
- package/docs/reports/v0.7-reference-compatibility-prompt4.md +234 -0
- package/docs/reports/v0.7-reference-correction-workflow-prompt8.md +222 -0
- package/docs/reports/v0.7-reference-fidelity-prompt6.md +216 -0
- package/docs/reports/v0.7-reference-foundation-prompt1.md +151 -0
- package/docs/reports/v0.7-reference-regions-prompt2.md +195 -0
- package/docs/reports/v0.7-reference-requirements-prompt3.md +217 -0
- package/docs/reports/v0.7-release-prep.md +423 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { observe } from './application/observationPersistence.js';
|
|
|
7
7
|
import { compareAndPersistFromArtifactRoots } from './application/comparisonService.js';
|
|
8
8
|
import { approveAndPersistBaseline, persistPerChangeContract } from './application/frontendContractPersistenceService.js';
|
|
9
9
|
import { evaluateAndPersistFromArtifactRoots } from './application/frontendContractEvaluationService.js';
|
|
10
|
+
import { importExternalReference, approveExternalReference } from './application/externalReferencePersistenceService.js';
|
|
11
|
+
import { evaluateReferenceCandidateFidelityFromArtifactRoots } from './application/referenceFidelityEvaluationService.js';
|
|
10
12
|
const defaultIO = {
|
|
11
13
|
stdout: (text) => {
|
|
12
14
|
process.stdout.write(text);
|
|
@@ -34,6 +36,18 @@ Commands:
|
|
|
34
36
|
baseline, a per-change contract, and existing
|
|
35
37
|
before/after/comparison evidence, and persist the
|
|
36
38
|
result.
|
|
39
|
+
import-reference Validate and persist one local external design-
|
|
40
|
+
reference image as a new, unapproved
|
|
41
|
+
external-reference artifact.
|
|
42
|
+
approve-reference Explicitly approve one already-imported
|
|
43
|
+
external-reference artifact, persisting a new
|
|
44
|
+
approved artifact instance.
|
|
45
|
+
evaluate-reference-fidelity Evaluate whether a candidate observation
|
|
46
|
+
satisfies an external reference's selected design
|
|
47
|
+
requirements, gated by reference adequacy,
|
|
48
|
+
reference/candidate compatibility, and explicit
|
|
49
|
+
region-to-target bindings. Prints a structured
|
|
50
|
+
result; persists nothing.
|
|
37
51
|
|
|
38
52
|
Options:
|
|
39
53
|
--help Show this help.
|
|
@@ -77,6 +91,19 @@ Options:
|
|
|
77
91
|
persisted into the artifact or included in the
|
|
78
92
|
observation's request identity. May be combined
|
|
79
93
|
with either --target or --targets-file.
|
|
94
|
+
--state-file <json-file> Loads explicit, caller-declared frontend state
|
|
95
|
+
identity from a local JSON file: { "theme":
|
|
96
|
+
"...", "applicationState": "...",
|
|
97
|
+
"authenticatedState": "authenticated"|
|
|
98
|
+
"unauthenticated" } (each field independently
|
|
99
|
+
optional; at least one required). Never inferred
|
|
100
|
+
by the observer from screenshot pixels, CSS, DOM,
|
|
101
|
+
or URLs - this is caller-declared metadata only,
|
|
102
|
+
used solely for later comparability/compatibility
|
|
103
|
+
evaluation. Relative paths resolve from the
|
|
104
|
+
current working directory; the file path itself
|
|
105
|
+
is never persisted into the artifact or included
|
|
106
|
+
in the observation's request identity.
|
|
80
107
|
--output <directory> Portable, relative output location for the
|
|
81
108
|
observation artifact.
|
|
82
109
|
--timeout <ms> Overall request timeout in milliseconds.
|
|
@@ -224,6 +251,172 @@ incoherent source artifact, or a persistence failure (evaluation could not
|
|
|
224
251
|
even be constructed), prints structured diagnostics to stderr, persists
|
|
225
252
|
nothing, and exits nonzero.
|
|
226
253
|
`;
|
|
254
|
+
const IMPORT_REFERENCE_HELP = `Usage:
|
|
255
|
+
my-frontend-observer import-reference <image-file> --output <directory> [options]
|
|
256
|
+
|
|
257
|
+
Required:
|
|
258
|
+
<image-file> Local path to a PNG, JPEG, or WebP external design-
|
|
259
|
+
reference image.
|
|
260
|
+
--output <directory> Portable, relative output location for the
|
|
261
|
+
external-reference artifact.
|
|
262
|
+
|
|
263
|
+
Options:
|
|
264
|
+
--label <text> Optional human-readable label, stored as pure
|
|
265
|
+
provenance - never part of the reference's logical
|
|
266
|
+
identity.
|
|
267
|
+
--supersedes <path> Root directory of a prior external-reference
|
|
268
|
+
artifact (imported or approved) that this import
|
|
269
|
+
explicitly supersedes. The prior artifact is never
|
|
270
|
+
modified.
|
|
271
|
+
--regions-file <json-file> Local JSON file of the form { "regions": [...] }
|
|
272
|
+
declaring explicit, meaningful reference-image
|
|
273
|
+
regions (id + a {x, y, width, height} rectangle in
|
|
274
|
+
reference-image pixels, origin at the image's
|
|
275
|
+
top-left corner). Optional - a reference imported
|
|
276
|
+
without this flag behaves exactly as in v0.7 Prompt
|
|
277
|
+
1. Region content participates in the reference's
|
|
278
|
+
logical identity; the file path itself never does.
|
|
279
|
+
--requirements-file <json-file> Local JSON file of the form
|
|
280
|
+
{ "requirements": [...] } declaring explicit,
|
|
281
|
+
user-selected design requirements over the regions
|
|
282
|
+
above - what actually matters for later candidate
|
|
283
|
+
evaluation, never inferred merely because a region
|
|
284
|
+
property/relationship exists. Each requirement has
|
|
285
|
+
a "category" (requested | expected-dependent |
|
|
286
|
+
protected | preserved - "unexpected" is never
|
|
287
|
+
authorable), a "subject" (a region property, a
|
|
288
|
+
region-to-region relationship, or a derived
|
|
289
|
+
two-region measurement), and - for property/
|
|
290
|
+
measurement subjects - a "tolerance" (exact |
|
|
291
|
+
absolute-reference-px | percent; relationship
|
|
292
|
+
subjects must omit tolerance). Requires --regions-
|
|
293
|
+
file (or an already-present region set) supplying
|
|
294
|
+
every region a requirement refers to. Optional -
|
|
295
|
+
a reference imported without this flag behaves
|
|
296
|
+
exactly as in v0.7 Prompt 1/2. Requirement content
|
|
297
|
+
participates in the reference's logical identity.
|
|
298
|
+
--applicability-file <json-file> Local JSON file declaring the runtime
|
|
299
|
+
frontend state this reference is intended to
|
|
300
|
+
represent: { "viewport": { "width", "height" },
|
|
301
|
+
"theme": "...", "applicationState": "...",
|
|
302
|
+
"authenticatedState": "authenticated"|
|
|
303
|
+
"unauthenticated" } (each field independently
|
|
304
|
+
optional; at least one required). "viewport" here
|
|
305
|
+
is the CSS-pixel runtime viewport the design
|
|
306
|
+
represents - distinct from the reference image's
|
|
307
|
+
own pixel dimensions, which are never assumed
|
|
308
|
+
equal. Never inferred from the image - caller-
|
|
309
|
+
declared metadata only, used for later reference/
|
|
310
|
+
candidate compatibility evaluation (see
|
|
311
|
+
docs/CONTRACTS.md "v0.7 Prompt 4"). Optional - a
|
|
312
|
+
reference imported without this flag behaves
|
|
313
|
+
exactly as in v0.7 Prompt 1/2/3. Applicability
|
|
314
|
+
content participates in the reference's logical
|
|
315
|
+
identity.
|
|
316
|
+
--help Show this help.
|
|
317
|
+
|
|
318
|
+
Detects the image format from its header bytes only (never from the file
|
|
319
|
+
extension), reads its pixel dimensions from the same bounded header bytes
|
|
320
|
+
(never decoding pixel data), and persists a new external-reference artifact
|
|
321
|
+
in the "imported" lifecycle state - importing never approves it. On success,
|
|
322
|
+
prints a concise result (including the accepted region/requirement counts,
|
|
323
|
+
the resulting reference-side requirement adequacy: adequate, partial, or
|
|
324
|
+
inadequate, and whether applicability was declared) and exits 0. On an
|
|
325
|
+
unreadable file, an unsupported or undetectable format, invalid/out-of-bound
|
|
326
|
+
dimensions, an over-limit file size, an unresolvable --supersedes target, an
|
|
327
|
+
invalid region (missing/duplicate/malformed id, non-finite/negative/zero
|
|
328
|
+
geometry, a region extending outside the image, or more than the bounded
|
|
329
|
+
maximum region count), an invalid requirement (unsupported category/
|
|
330
|
+
property/measurement/relationship, a tolerance that is missing/inapplicable/
|
|
331
|
+
out of bounds, a reference to an unknown region id, a duplicate requirement
|
|
332
|
+
subject, or more than the bounded maximum requirement count), or invalid
|
|
333
|
+
applicability (an out-of-bound viewport, an invalid state label, an
|
|
334
|
+
unsupported authenticatedState value, or an empty applicability object),
|
|
335
|
+
prints structured diagnostics to stderr and exits nonzero.
|
|
336
|
+
`;
|
|
337
|
+
const APPROVE_REFERENCE_HELP = `Usage:
|
|
338
|
+
my-frontend-observer approve-reference --reference <external-reference-artifact-root> --output <directory> [options]
|
|
339
|
+
|
|
340
|
+
Required:
|
|
341
|
+
--reference <path> Root directory of the already-imported
|
|
342
|
+
external-reference artifact (the directory
|
|
343
|
+
containing its manifest.json) to approve.
|
|
344
|
+
--output <directory> Portable, relative output location for the newly
|
|
345
|
+
persisted approved artifact.
|
|
346
|
+
|
|
347
|
+
Options:
|
|
348
|
+
--supersedes <path> Root directory of a prior external-reference
|
|
349
|
+
artifact (imported or approved) that this approval
|
|
350
|
+
explicitly supersedes. The prior artifact is never
|
|
351
|
+
modified.
|
|
352
|
+
--help Show this help.
|
|
353
|
+
|
|
354
|
+
This is the only explicit reference-approval act in the observer - approval
|
|
355
|
+
is never inferred from a successful import or from any later fidelity
|
|
356
|
+
evaluation. Approving persists a brand-new artifact instance (a fresh
|
|
357
|
+
referenceId sharing the imported artifact's referenceRequestId) that carries
|
|
358
|
+
a reference back to the imported artifact's image rather than a second copy
|
|
359
|
+
of its bytes; the imported artifact's own manifest is never modified. Any
|
|
360
|
+
regions, requirements, and applicability already declared on the imported
|
|
361
|
+
artifact are carried forward unchanged (not re-validated against new input,
|
|
362
|
+
not re-derived) - approval never adds, removes, or edits regions,
|
|
363
|
+
requirements, or applicability. Only a reference currently in the "imported"
|
|
364
|
+
lifecycle state can be approved. On success, prints a concise result
|
|
365
|
+
(including the carried-forward region/requirement counts, reference-side
|
|
366
|
+
requirement adequacy, and whether applicability was declared) and exits 0.
|
|
367
|
+
On an unreadable/malformed --reference target, a target that is not in the
|
|
368
|
+
"imported" state, an unresolvable --supersedes target, or a persistence
|
|
369
|
+
failure, prints structured diagnostics to stderr and exits nonzero.
|
|
370
|
+
`;
|
|
371
|
+
const EVALUATE_REFERENCE_FIDELITY_HELP = `Usage:
|
|
372
|
+
my-frontend-observer evaluate-reference-fidelity --reference <external-reference-artifact-root> --candidate <observation-artifact-root> [options]
|
|
373
|
+
|
|
374
|
+
Required:
|
|
375
|
+
--reference <path> Root directory of an already-imported or already-
|
|
376
|
+
approved external-reference artifact (the directory
|
|
377
|
+
containing its manifest.json).
|
|
378
|
+
--candidate <path> Root directory of the already-persisted candidate
|
|
379
|
+
observation artifact to evaluate against it.
|
|
380
|
+
|
|
381
|
+
Options:
|
|
382
|
+
--bindings-file <json-file> Local JSON file of the form
|
|
383
|
+
{ "bindings": [ { "referenceRegion": "...",
|
|
384
|
+
"runtimeTarget": "..." } ] } declaring which stable
|
|
385
|
+
observer runtime target (a configured target name -
|
|
386
|
+
see "observe" --target/--targets-file) explicitly
|
|
387
|
+
corresponds to each reference region a selected
|
|
388
|
+
requirement depends on. Never inferred from
|
|
389
|
+
geometry, matching names, or source code - a
|
|
390
|
+
binding exists only because this file declares it.
|
|
391
|
+
Optional - omitting it (or supplying an empty
|
|
392
|
+
"bindings" array) evaluates with no bindings at
|
|
393
|
+
all, so every requirement whose subject depends on
|
|
394
|
+
a reference region becomes "unavailable".
|
|
395
|
+
--enforce Make a FAIL fidelity result produce a nonzero process exit
|
|
396
|
+
status. A FAIL result is always printed identically with or
|
|
397
|
+
without this flag - it changes only the process exit code,
|
|
398
|
+
never the evaluation's content. Has no effect on a
|
|
399
|
+
"not-evaluated" result (a reference-adequacy or compatibility
|
|
400
|
+
blocker is never treated as a design mismatch).
|
|
401
|
+
--help Show this help.
|
|
402
|
+
|
|
403
|
+
This command never launches a browser, never re-resolves targets, and
|
|
404
|
+
never recomputes reference regions/requirements/adequacy, compatibility, or
|
|
405
|
+
bindings - it reads the already-persisted reference and candidate exactly
|
|
406
|
+
as given, evaluates the supplied binding declarations, and evaluates every
|
|
407
|
+
one of the reference's selected requirements exactly once. It persists
|
|
408
|
+
nothing: the result exists only for this invocation. On success, prints a
|
|
409
|
+
concise result (reference-side adequacy, compatibility state, the overall
|
|
410
|
+
fidelity state - "not-evaluated"/"pass"/"fail" - and a pass/fail/unavailable
|
|
411
|
+
requirement breakdown) and exits 0, unless --enforce is given and the
|
|
412
|
+
fidelity state is "fail", in which case it exits nonzero. A "not-evaluated"
|
|
413
|
+
result (reference adequacy inadequate, or reference/candidate
|
|
414
|
+
incompatible) is a successful, structured evaluation outcome, never an
|
|
415
|
+
execution error - it always exits 0. On invalid syntax, an unreadable/
|
|
416
|
+
malformed --reference or --candidate target, a malformed --bindings-file,
|
|
417
|
+
or an invalid/out-of-bound binding declaration, prints structured
|
|
418
|
+
diagnostics to stderr and exits nonzero.
|
|
419
|
+
`;
|
|
227
420
|
function parseViewport(raw) {
|
|
228
421
|
const match = /^(\d+)x(\d+)$/.exec(raw);
|
|
229
422
|
if (!match)
|
|
@@ -250,6 +443,8 @@ function parseObserveArgs(argv) {
|
|
|
250
443
|
let targetsFileFlagCount = 0;
|
|
251
444
|
let scrollScenarioFilePath;
|
|
252
445
|
let scrollScenarioFileFlagCount = 0;
|
|
446
|
+
let stateFilePath;
|
|
447
|
+
let stateFileFlagCount = 0;
|
|
253
448
|
for (let i = 0; i < argv.length; i += 1) {
|
|
254
449
|
const arg = argv[i];
|
|
255
450
|
switch (arg) {
|
|
@@ -306,6 +501,20 @@ function parseObserveArgs(argv) {
|
|
|
306
501
|
}
|
|
307
502
|
break;
|
|
308
503
|
}
|
|
504
|
+
case '--state-file': {
|
|
505
|
+
const value = argv[(i += 1)];
|
|
506
|
+
stateFileFlagCount += 1;
|
|
507
|
+
if (value === undefined) {
|
|
508
|
+
errors.push('--state-file requires a file path argument');
|
|
509
|
+
}
|
|
510
|
+
else if (stateFileFlagCount > 1) {
|
|
511
|
+
errors.push('--state-file may only be specified once');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
stateFilePath = value;
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
309
518
|
case '--output':
|
|
310
519
|
outputLocation = argv[(i += 1)];
|
|
311
520
|
break;
|
|
@@ -343,9 +552,169 @@ function parseObserveArgs(argv) {
|
|
|
343
552
|
raw,
|
|
344
553
|
...(targetsFilePath === undefined ? {} : { targetsFilePath }),
|
|
345
554
|
...(scrollScenarioFilePath === undefined ? {} : { scrollScenarioFilePath }),
|
|
555
|
+
...(stateFilePath === undefined ? {} : { stateFilePath }),
|
|
346
556
|
};
|
|
347
557
|
}
|
|
348
558
|
const TARGETS_FILE_ALLOWED_ROOT_FIELDS = new Set(['targets']);
|
|
559
|
+
const REGIONS_FILE_ALLOWED_ROOT_FIELDS = new Set(['regions']);
|
|
560
|
+
/**
|
|
561
|
+
* CLI/input-boundary-only responsibility, mirroring loadTargetsFile exactly:
|
|
562
|
+
* read one local JSON file, validate only the root wrapper (object root,
|
|
563
|
+
* exactly the "regions" field, nothing else), and hand the still-unvalidated
|
|
564
|
+
* `regions` value to the existing domain validators
|
|
565
|
+
* (isValidReferenceRegions, called inside importExternalReference) - region
|
|
566
|
+
* geometry/ID rules stay owned there, never duplicated here. The file path
|
|
567
|
+
* itself is never returned beyond this function, so it can never reach the
|
|
568
|
+
* persisted artifact or its identity.
|
|
569
|
+
*/
|
|
570
|
+
function loadRegionsFile(filePath) {
|
|
571
|
+
let rawText;
|
|
572
|
+
try {
|
|
573
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
577
|
+
return { ok: false, error: `--regions-file could not be read: ${message}` };
|
|
578
|
+
}
|
|
579
|
+
let parsed;
|
|
580
|
+
try {
|
|
581
|
+
parsed = JSON.parse(rawText);
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
585
|
+
return { ok: false, error: `--regions-file is not valid JSON: ${message}` };
|
|
586
|
+
}
|
|
587
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
588
|
+
return { ok: false, error: '--regions-file root must be a JSON object' };
|
|
589
|
+
}
|
|
590
|
+
const record = parsed;
|
|
591
|
+
const unknownFields = Object.keys(record).filter((key) => !REGIONS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
592
|
+
if (unknownFields.length > 0) {
|
|
593
|
+
return { ok: false, error: `--regions-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
594
|
+
}
|
|
595
|
+
if (!('regions' in record)) {
|
|
596
|
+
return { ok: false, error: '--regions-file must have a "regions" property' };
|
|
597
|
+
}
|
|
598
|
+
return { ok: true, regions: record.regions };
|
|
599
|
+
}
|
|
600
|
+
const REQUIREMENTS_FILE_ALLOWED_ROOT_FIELDS = new Set(['requirements']);
|
|
601
|
+
/**
|
|
602
|
+
* CLI/input-boundary-only responsibility, mirroring loadRegionsFile exactly:
|
|
603
|
+
* read one local JSON file, validate only the root wrapper (object root,
|
|
604
|
+
* exactly the "requirements" field, nothing else), and hand the
|
|
605
|
+
* still-unvalidated `requirements` value to the existing domain validators
|
|
606
|
+
* (isValidRawReferenceRequirement/isValidReferenceRequirements, called
|
|
607
|
+
* inside importExternalReference) - requirement category/subject/tolerance
|
|
608
|
+
* rules stay owned there, never duplicated here. The file path itself is
|
|
609
|
+
* never returned beyond this function, so it can never reach the persisted
|
|
610
|
+
* artifact or its identity.
|
|
611
|
+
*/
|
|
612
|
+
function loadRequirementsFile(filePath) {
|
|
613
|
+
let rawText;
|
|
614
|
+
try {
|
|
615
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
619
|
+
return { ok: false, error: `--requirements-file could not be read: ${message}` };
|
|
620
|
+
}
|
|
621
|
+
let parsed;
|
|
622
|
+
try {
|
|
623
|
+
parsed = JSON.parse(rawText);
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
627
|
+
return { ok: false, error: `--requirements-file is not valid JSON: ${message}` };
|
|
628
|
+
}
|
|
629
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
630
|
+
return { ok: false, error: '--requirements-file root must be a JSON object' };
|
|
631
|
+
}
|
|
632
|
+
const record = parsed;
|
|
633
|
+
const unknownFields = Object.keys(record).filter((key) => !REQUIREMENTS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
634
|
+
if (unknownFields.length > 0) {
|
|
635
|
+
return { ok: false, error: `--requirements-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
636
|
+
}
|
|
637
|
+
if (!('requirements' in record)) {
|
|
638
|
+
return { ok: false, error: '--requirements-file must have a "requirements" property' };
|
|
639
|
+
}
|
|
640
|
+
return { ok: true, requirements: record.requirements };
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* CLI/input-boundary-only responsibility, mirroring `loadStateFile`/
|
|
644
|
+
* `loadScrollScenarioFile`: read one local JSON file and validate only the
|
|
645
|
+
* root shape (plain, non-array object) - the file supplies
|
|
646
|
+
* `ExternalReferenceApplicability` directly (no wrapper field), so there is
|
|
647
|
+
* no root-field allowlist to enforce here. Every applicability rule
|
|
648
|
+
* (viewport bounds, state-label pattern, authenticatedState enum) stays
|
|
649
|
+
* owned by `isValidExternalReferenceApplicability`, not duplicated here. The
|
|
650
|
+
* file path itself is never returned beyond this function, so it can never
|
|
651
|
+
* reach the persisted artifact or its identity.
|
|
652
|
+
*/
|
|
653
|
+
function loadApplicabilityFile(filePath) {
|
|
654
|
+
let rawText;
|
|
655
|
+
try {
|
|
656
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
660
|
+
return { ok: false, error: `--applicability-file could not be read: ${message}` };
|
|
661
|
+
}
|
|
662
|
+
let parsed;
|
|
663
|
+
try {
|
|
664
|
+
parsed = JSON.parse(rawText);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
668
|
+
return { ok: false, error: `--applicability-file is not valid JSON: ${message}` };
|
|
669
|
+
}
|
|
670
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
671
|
+
return { ok: false, error: '--applicability-file root must be a JSON object' };
|
|
672
|
+
}
|
|
673
|
+
return { ok: true, applicability: parsed };
|
|
674
|
+
}
|
|
675
|
+
const BINDINGS_FILE_ALLOWED_ROOT_FIELDS = new Set(['bindings']);
|
|
676
|
+
/**
|
|
677
|
+
* CLI/input-boundary-only responsibility, mirroring `loadRegionsFile`/
|
|
678
|
+
* `loadRequirementsFile` exactly: read one local JSON file, validate only
|
|
679
|
+
* the root wrapper (object root, exactly the "bindings" property, nothing
|
|
680
|
+
* else), and hand the still-unvalidated `bindings` value to the existing
|
|
681
|
+
* domain validator (`isValidReferenceRuntimeBindingDeclarations`, called
|
|
682
|
+
* inside `evaluateReferenceCandidateFidelity`) - every binding-declaration
|
|
683
|
+
* rule (shape, bounds, reference-region existence, duplicate/conflict)
|
|
684
|
+
* stays owned there, never duplicated here. The file path itself is never
|
|
685
|
+
* returned beyond this function, so it can never reach the evaluation
|
|
686
|
+
* result or any identity.
|
|
687
|
+
*/
|
|
688
|
+
function loadBindingsFile(filePath) {
|
|
689
|
+
let rawText;
|
|
690
|
+
try {
|
|
691
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
695
|
+
return { ok: false, error: `--bindings-file could not be read: ${message}` };
|
|
696
|
+
}
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(rawText);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
703
|
+
return { ok: false, error: `--bindings-file is not valid JSON: ${message}` };
|
|
704
|
+
}
|
|
705
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
706
|
+
return { ok: false, error: '--bindings-file root must be a JSON object' };
|
|
707
|
+
}
|
|
708
|
+
const record = parsed;
|
|
709
|
+
const unknownFields = Object.keys(record).filter((key) => !BINDINGS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
710
|
+
if (unknownFields.length > 0) {
|
|
711
|
+
return { ok: false, error: `--bindings-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
712
|
+
}
|
|
713
|
+
if (!('bindings' in record)) {
|
|
714
|
+
return { ok: false, error: '--bindings-file must have a "bindings" property' };
|
|
715
|
+
}
|
|
716
|
+
return { ok: true, bindings: record.bindings };
|
|
717
|
+
}
|
|
349
718
|
/**
|
|
350
719
|
* CLI/input-boundary-only responsibility: read one local JSON file, validate
|
|
351
720
|
* only the root wrapper this file format owns (object root, exactly the
|
|
@@ -420,6 +789,39 @@ function loadScrollScenarioFile(filePath) {
|
|
|
420
789
|
}
|
|
421
790
|
return { ok: true, scenario: parsed };
|
|
422
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* CLI/input-boundary-only responsibility, mirroring `loadScrollScenarioFile`
|
|
794
|
+
* exactly: read one local JSON file and validate only the root shape (plain,
|
|
795
|
+
* non-array object) - the file supplies the value of
|
|
796
|
+
* `RawObservationRequest.explicitState` directly (no wrapper field), so
|
|
797
|
+
* there is no root-field allowlist to enforce here. Every state rule
|
|
798
|
+
* (supported dimension keys, label pattern, authenticatedState enum) stays
|
|
799
|
+
* owned by `normalizeRequest()`/`isValidExplicitStateDimensions`, not
|
|
800
|
+
* duplicated here. The file path itself is never returned to the caller
|
|
801
|
+
* beyond this function, so it can never reach the persisted request/artifact.
|
|
802
|
+
*/
|
|
803
|
+
function loadStateFile(filePath) {
|
|
804
|
+
let rawText;
|
|
805
|
+
try {
|
|
806
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
810
|
+
return { ok: false, error: `--state-file could not be read: ${message}` };
|
|
811
|
+
}
|
|
812
|
+
let parsed;
|
|
813
|
+
try {
|
|
814
|
+
parsed = JSON.parse(rawText);
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
818
|
+
return { ok: false, error: `--state-file is not valid JSON: ${message}` };
|
|
819
|
+
}
|
|
820
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
821
|
+
return { ok: false, error: '--state-file root must be a JSON object' };
|
|
822
|
+
}
|
|
823
|
+
return { ok: true, state: parsed };
|
|
824
|
+
}
|
|
423
825
|
/** CLI-syntax-only parsing, mirroring `parseObserveArgs`: shape/presence/duplication errors only. Comparison-config semantics stay owned by the existing domain validator. */
|
|
424
826
|
function parseCompareArgs(argv) {
|
|
425
827
|
const errors = [];
|
|
@@ -798,6 +1200,248 @@ function parseEvaluateContractArgs(argv) {
|
|
|
798
1200
|
enforce,
|
|
799
1201
|
};
|
|
800
1202
|
}
|
|
1203
|
+
/** CLI-syntax-only parsing, mirroring `parseApproveBaselineArgs`. The image file path is the one positional argument. */
|
|
1204
|
+
function parseImportReferenceArgs(argv) {
|
|
1205
|
+
const errors = [];
|
|
1206
|
+
let imageFilePath;
|
|
1207
|
+
let outputLocation;
|
|
1208
|
+
let outputFlagCount = 0;
|
|
1209
|
+
let label;
|
|
1210
|
+
let labelFlagCount = 0;
|
|
1211
|
+
let supersedesReferenceRoot;
|
|
1212
|
+
let supersedesFlagCount = 0;
|
|
1213
|
+
let regionsFilePath;
|
|
1214
|
+
let regionsFileFlagCount = 0;
|
|
1215
|
+
let requirementsFilePath;
|
|
1216
|
+
let requirementsFileFlagCount = 0;
|
|
1217
|
+
let applicabilityFilePath;
|
|
1218
|
+
let applicabilityFileFlagCount = 0;
|
|
1219
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1220
|
+
const arg = argv[i];
|
|
1221
|
+
switch (arg) {
|
|
1222
|
+
case '--output': {
|
|
1223
|
+
const value = argv[(i += 1)];
|
|
1224
|
+
outputFlagCount += 1;
|
|
1225
|
+
if (value === undefined)
|
|
1226
|
+
errors.push('--output requires a directory argument');
|
|
1227
|
+
else if (outputFlagCount > 1)
|
|
1228
|
+
errors.push('--output may only be specified once');
|
|
1229
|
+
else
|
|
1230
|
+
outputLocation = value;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
case '--label': {
|
|
1234
|
+
const value = argv[(i += 1)];
|
|
1235
|
+
labelFlagCount += 1;
|
|
1236
|
+
if (value === undefined)
|
|
1237
|
+
errors.push('--label requires a text argument');
|
|
1238
|
+
else if (labelFlagCount > 1)
|
|
1239
|
+
errors.push('--label may only be specified once');
|
|
1240
|
+
else
|
|
1241
|
+
label = value;
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
case '--supersedes': {
|
|
1245
|
+
const value = argv[(i += 1)];
|
|
1246
|
+
supersedesFlagCount += 1;
|
|
1247
|
+
if (value === undefined)
|
|
1248
|
+
errors.push('--supersedes requires a path argument');
|
|
1249
|
+
else if (supersedesFlagCount > 1)
|
|
1250
|
+
errors.push('--supersedes may only be specified once');
|
|
1251
|
+
else
|
|
1252
|
+
supersedesReferenceRoot = value;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
case '--regions-file': {
|
|
1256
|
+
const value = argv[(i += 1)];
|
|
1257
|
+
regionsFileFlagCount += 1;
|
|
1258
|
+
if (value === undefined)
|
|
1259
|
+
errors.push('--regions-file requires a file path argument');
|
|
1260
|
+
else if (regionsFileFlagCount > 1)
|
|
1261
|
+
errors.push('--regions-file may only be specified once');
|
|
1262
|
+
else
|
|
1263
|
+
regionsFilePath = value;
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
case '--requirements-file': {
|
|
1267
|
+
const value = argv[(i += 1)];
|
|
1268
|
+
requirementsFileFlagCount += 1;
|
|
1269
|
+
if (value === undefined)
|
|
1270
|
+
errors.push('--requirements-file requires a file path argument');
|
|
1271
|
+
else if (requirementsFileFlagCount > 1)
|
|
1272
|
+
errors.push('--requirements-file may only be specified once');
|
|
1273
|
+
else
|
|
1274
|
+
requirementsFilePath = value;
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
case '--applicability-file': {
|
|
1278
|
+
const value = argv[(i += 1)];
|
|
1279
|
+
applicabilityFileFlagCount += 1;
|
|
1280
|
+
if (value === undefined)
|
|
1281
|
+
errors.push('--applicability-file requires a file path argument');
|
|
1282
|
+
else if (applicabilityFileFlagCount > 1)
|
|
1283
|
+
errors.push('--applicability-file may only be specified once');
|
|
1284
|
+
else
|
|
1285
|
+
applicabilityFilePath = value;
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
default:
|
|
1289
|
+
if (arg === undefined)
|
|
1290
|
+
break;
|
|
1291
|
+
if (arg.startsWith('--'))
|
|
1292
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1293
|
+
else if (imageFilePath !== undefined)
|
|
1294
|
+
errors.push('only one image-file argument may be given');
|
|
1295
|
+
else
|
|
1296
|
+
imageFilePath = arg;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (imageFilePath === undefined)
|
|
1300
|
+
errors.push('an image-file argument is required');
|
|
1301
|
+
if (outputLocation === undefined)
|
|
1302
|
+
errors.push('--output is required');
|
|
1303
|
+
if (errors.length > 0)
|
|
1304
|
+
return { ok: false, errors };
|
|
1305
|
+
return {
|
|
1306
|
+
ok: true,
|
|
1307
|
+
imageFilePath: imageFilePath,
|
|
1308
|
+
outputLocation: outputLocation,
|
|
1309
|
+
...(label === undefined ? {} : { label }),
|
|
1310
|
+
...(supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot }),
|
|
1311
|
+
...(regionsFilePath === undefined ? {} : { regionsFilePath }),
|
|
1312
|
+
...(requirementsFilePath === undefined ? {} : { requirementsFilePath }),
|
|
1313
|
+
...(applicabilityFilePath === undefined ? {} : { applicabilityFilePath }),
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
/** CLI-syntax-only parsing, mirroring `parseApproveBaselineArgs`. */
|
|
1317
|
+
function parseApproveReferenceArgs(argv) {
|
|
1318
|
+
const errors = [];
|
|
1319
|
+
let referenceRoot;
|
|
1320
|
+
let referenceFlagCount = 0;
|
|
1321
|
+
let outputLocation;
|
|
1322
|
+
let outputFlagCount = 0;
|
|
1323
|
+
let supersedesReferenceRoot;
|
|
1324
|
+
let supersedesFlagCount = 0;
|
|
1325
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1326
|
+
const arg = argv[i];
|
|
1327
|
+
switch (arg) {
|
|
1328
|
+
case '--reference': {
|
|
1329
|
+
const value = argv[(i += 1)];
|
|
1330
|
+
referenceFlagCount += 1;
|
|
1331
|
+
if (value === undefined)
|
|
1332
|
+
errors.push('--reference requires a path argument');
|
|
1333
|
+
else if (referenceFlagCount > 1)
|
|
1334
|
+
errors.push('--reference may only be specified once');
|
|
1335
|
+
else
|
|
1336
|
+
referenceRoot = value;
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
case '--output': {
|
|
1340
|
+
const value = argv[(i += 1)];
|
|
1341
|
+
outputFlagCount += 1;
|
|
1342
|
+
if (value === undefined)
|
|
1343
|
+
errors.push('--output requires a directory argument');
|
|
1344
|
+
else if (outputFlagCount > 1)
|
|
1345
|
+
errors.push('--output may only be specified once');
|
|
1346
|
+
else
|
|
1347
|
+
outputLocation = value;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
case '--supersedes': {
|
|
1351
|
+
const value = argv[(i += 1)];
|
|
1352
|
+
supersedesFlagCount += 1;
|
|
1353
|
+
if (value === undefined)
|
|
1354
|
+
errors.push('--supersedes requires a path argument');
|
|
1355
|
+
else if (supersedesFlagCount > 1)
|
|
1356
|
+
errors.push('--supersedes may only be specified once');
|
|
1357
|
+
else
|
|
1358
|
+
supersedesReferenceRoot = value;
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
default:
|
|
1362
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (referenceRoot === undefined)
|
|
1366
|
+
errors.push('--reference is required');
|
|
1367
|
+
if (outputLocation === undefined)
|
|
1368
|
+
errors.push('--output is required');
|
|
1369
|
+
if (errors.length > 0)
|
|
1370
|
+
return { ok: false, errors };
|
|
1371
|
+
return {
|
|
1372
|
+
ok: true,
|
|
1373
|
+
referenceRoot: referenceRoot,
|
|
1374
|
+
outputLocation: outputLocation,
|
|
1375
|
+
...(supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot }),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
/** CLI-syntax-only parsing, mirroring `parseEvaluateContractArgs`'s `--enforce` handling exactly. */
|
|
1379
|
+
function parseEvaluateReferenceFidelityArgs(argv) {
|
|
1380
|
+
const errors = [];
|
|
1381
|
+
let referenceRoot;
|
|
1382
|
+
let referenceFlagCount = 0;
|
|
1383
|
+
let candidateRoot;
|
|
1384
|
+
let candidateFlagCount = 0;
|
|
1385
|
+
let bindingsFilePath;
|
|
1386
|
+
let bindingsFileFlagCount = 0;
|
|
1387
|
+
let enforce = false;
|
|
1388
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1389
|
+
const arg = argv[i];
|
|
1390
|
+
switch (arg) {
|
|
1391
|
+
case '--reference': {
|
|
1392
|
+
const value = argv[(i += 1)];
|
|
1393
|
+
referenceFlagCount += 1;
|
|
1394
|
+
if (value === undefined)
|
|
1395
|
+
errors.push('--reference requires a path argument');
|
|
1396
|
+
else if (referenceFlagCount > 1)
|
|
1397
|
+
errors.push('--reference may only be specified once');
|
|
1398
|
+
else
|
|
1399
|
+
referenceRoot = value;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
case '--candidate': {
|
|
1403
|
+
const value = argv[(i += 1)];
|
|
1404
|
+
candidateFlagCount += 1;
|
|
1405
|
+
if (value === undefined)
|
|
1406
|
+
errors.push('--candidate requires a path argument');
|
|
1407
|
+
else if (candidateFlagCount > 1)
|
|
1408
|
+
errors.push('--candidate may only be specified once');
|
|
1409
|
+
else
|
|
1410
|
+
candidateRoot = value;
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
case '--bindings-file': {
|
|
1414
|
+
const value = argv[(i += 1)];
|
|
1415
|
+
bindingsFileFlagCount += 1;
|
|
1416
|
+
if (value === undefined)
|
|
1417
|
+
errors.push('--bindings-file requires a file path argument');
|
|
1418
|
+
else if (bindingsFileFlagCount > 1)
|
|
1419
|
+
errors.push('--bindings-file may only be specified once');
|
|
1420
|
+
else
|
|
1421
|
+
bindingsFilePath = value;
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
case '--enforce':
|
|
1425
|
+
enforce = true;
|
|
1426
|
+
break;
|
|
1427
|
+
default:
|
|
1428
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (referenceRoot === undefined)
|
|
1432
|
+
errors.push('--reference is required');
|
|
1433
|
+
if (candidateRoot === undefined)
|
|
1434
|
+
errors.push('--candidate is required');
|
|
1435
|
+
if (errors.length > 0)
|
|
1436
|
+
return { ok: false, errors };
|
|
1437
|
+
return {
|
|
1438
|
+
ok: true,
|
|
1439
|
+
referenceRoot: referenceRoot,
|
|
1440
|
+
candidateRoot: candidateRoot,
|
|
1441
|
+
...(bindingsFilePath === undefined ? {} : { bindingsFilePath }),
|
|
1442
|
+
enforce,
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
801
1445
|
function formatDiagnostic(diagnostic) {
|
|
802
1446
|
const target = diagnostic.targetName === undefined ? '' : ` (target: ${diagnostic.targetName})`;
|
|
803
1447
|
return `[${diagnostic.code}] ${diagnostic.message}${target}`;
|
|
@@ -835,6 +1479,15 @@ async function runObserveCommand(argv, io) {
|
|
|
835
1479
|
}
|
|
836
1480
|
raw = { ...raw, scrollScenario: loaded.scenario };
|
|
837
1481
|
}
|
|
1482
|
+
if (parsedArgs.stateFilePath !== undefined) {
|
|
1483
|
+
const loaded = loadStateFile(parsedArgs.stateFilePath);
|
|
1484
|
+
if (!loaded.ok) {
|
|
1485
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1486
|
+
io.stderr(OBSERVE_HELP);
|
|
1487
|
+
return 1;
|
|
1488
|
+
}
|
|
1489
|
+
raw = { ...raw, explicitState: loaded.state };
|
|
1490
|
+
}
|
|
838
1491
|
const normalized = normalizeRequest(raw);
|
|
839
1492
|
if (!normalized.ok) {
|
|
840
1493
|
for (const diagnostic of normalized.diagnostics)
|
|
@@ -1023,6 +1676,188 @@ async function runEvaluateContractCommand(argv, io) {
|
|
|
1023
1676
|
return 1;
|
|
1024
1677
|
return 0;
|
|
1025
1678
|
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Thin orchestration only: parse args, read the local image file's raw
|
|
1681
|
+
* bytes, then delegate to the existing `importExternalReference` application
|
|
1682
|
+
* function exactly once. No format/dimension validation lives here - see
|
|
1683
|
+
* `src/domain/externalReferenceImage.ts`.
|
|
1684
|
+
*/
|
|
1685
|
+
async function runImportReferenceCommand(argv, io) {
|
|
1686
|
+
if (argv.includes('--help')) {
|
|
1687
|
+
io.stdout(IMPORT_REFERENCE_HELP);
|
|
1688
|
+
return 0;
|
|
1689
|
+
}
|
|
1690
|
+
const parsedArgs = parseImportReferenceArgs(argv);
|
|
1691
|
+
if (!parsedArgs.ok) {
|
|
1692
|
+
for (const error of parsedArgs.errors)
|
|
1693
|
+
io.stderr(`error: ${error}\n`);
|
|
1694
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1695
|
+
return 1;
|
|
1696
|
+
}
|
|
1697
|
+
let imageBytes;
|
|
1698
|
+
try {
|
|
1699
|
+
imageBytes = readFileSync(parsedArgs.imageFilePath);
|
|
1700
|
+
}
|
|
1701
|
+
catch (err) {
|
|
1702
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1703
|
+
io.stderr(`error: could not read image file "${parsedArgs.imageFilePath}": ${message}\n`);
|
|
1704
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1705
|
+
return 1;
|
|
1706
|
+
}
|
|
1707
|
+
let regions;
|
|
1708
|
+
if (parsedArgs.regionsFilePath !== undefined) {
|
|
1709
|
+
const loaded = loadRegionsFile(parsedArgs.regionsFilePath);
|
|
1710
|
+
if (!loaded.ok) {
|
|
1711
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1712
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1713
|
+
return 1;
|
|
1714
|
+
}
|
|
1715
|
+
// CLI boundary owns file-read/root-wrapper syntax only; region content/geometry validation is owned by isValidReferenceRegions, called inside importExternalReference.
|
|
1716
|
+
regions = loaded.regions;
|
|
1717
|
+
}
|
|
1718
|
+
let requirements;
|
|
1719
|
+
if (parsedArgs.requirementsFilePath !== undefined) {
|
|
1720
|
+
const loaded = loadRequirementsFile(parsedArgs.requirementsFilePath);
|
|
1721
|
+
if (!loaded.ok) {
|
|
1722
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1723
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1724
|
+
return 1;
|
|
1725
|
+
}
|
|
1726
|
+
// CLI boundary owns file-read/root-wrapper syntax only; requirement category/subject/tolerance validation is owned by isValidRawReferenceRequirement/isValidReferenceRequirements, called inside importExternalReference.
|
|
1727
|
+
requirements = loaded.requirements;
|
|
1728
|
+
}
|
|
1729
|
+
let applicability;
|
|
1730
|
+
if (parsedArgs.applicabilityFilePath !== undefined) {
|
|
1731
|
+
const loaded = loadApplicabilityFile(parsedArgs.applicabilityFilePath);
|
|
1732
|
+
if (!loaded.ok) {
|
|
1733
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1734
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1735
|
+
return 1;
|
|
1736
|
+
}
|
|
1737
|
+
// CLI boundary owns file-read syntax only; applicability semantics are owned by isValidExternalReferenceApplicability, called inside importExternalReference.
|
|
1738
|
+
applicability = loaded.applicability;
|
|
1739
|
+
}
|
|
1740
|
+
// Exactly one application import attempt: format/dimension validation, an optional region-set validation, an optional requirement-set validation, an optional applicability validation, an optional supersession-target read, persisted at most once.
|
|
1741
|
+
const result = await importExternalReference(imageBytes, {
|
|
1742
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1743
|
+
...(parsedArgs.label === undefined ? {} : { label: parsedArgs.label }),
|
|
1744
|
+
...(parsedArgs.supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot: parsedArgs.supersedesReferenceRoot }),
|
|
1745
|
+
...(regions === undefined ? {} : { regions }),
|
|
1746
|
+
...(requirements === undefined ? {} : { requirements }),
|
|
1747
|
+
...(applicability === undefined ? {} : { applicability }),
|
|
1748
|
+
});
|
|
1749
|
+
if (!result.ok) {
|
|
1750
|
+
for (const diagnostic of result.diagnostics)
|
|
1751
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1752
|
+
return 1;
|
|
1753
|
+
}
|
|
1754
|
+
io.stdout(`Reference: ${result.referenceId}\n`);
|
|
1755
|
+
io.stdout(`State: imported\n`);
|
|
1756
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1757
|
+
io.stdout(`Image: ${result.imagePath}\n`);
|
|
1758
|
+
io.stdout(`Regions: ${result.regionCount}\n`);
|
|
1759
|
+
io.stdout(`Requirements: ${result.requirementCount}\n`);
|
|
1760
|
+
io.stdout(`Applicability: ${result.hasApplicability ? 'declared' : 'none'}\n`);
|
|
1761
|
+
io.stdout(`Adequacy: ${result.adequacy.status}\n`);
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Thin orchestration only: parse args, then delegate to the existing
|
|
1766
|
+
* `approveExternalReference` application function exactly once. This is the
|
|
1767
|
+
* only command in the observer that approves an external reference.
|
|
1768
|
+
*/
|
|
1769
|
+
async function runApproveReferenceCommand(argv, io) {
|
|
1770
|
+
if (argv.includes('--help')) {
|
|
1771
|
+
io.stdout(APPROVE_REFERENCE_HELP);
|
|
1772
|
+
return 0;
|
|
1773
|
+
}
|
|
1774
|
+
const parsedArgs = parseApproveReferenceArgs(argv);
|
|
1775
|
+
if (!parsedArgs.ok) {
|
|
1776
|
+
for (const error of parsedArgs.errors)
|
|
1777
|
+
io.stderr(`error: ${error}\n`);
|
|
1778
|
+
io.stderr(APPROVE_REFERENCE_HELP);
|
|
1779
|
+
return 1;
|
|
1780
|
+
}
|
|
1781
|
+
// Exactly one application approval attempt: one reference read, an optional supersession-target read, persisted at most once.
|
|
1782
|
+
const result = await approveExternalReference(parsedArgs.referenceRoot, {
|
|
1783
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1784
|
+
...(parsedArgs.supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot: parsedArgs.supersedesReferenceRoot }),
|
|
1785
|
+
});
|
|
1786
|
+
if (!result.ok) {
|
|
1787
|
+
for (const diagnostic of result.diagnostics)
|
|
1788
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1789
|
+
return 1;
|
|
1790
|
+
}
|
|
1791
|
+
io.stdout(`Reference: ${result.referenceId}\n`);
|
|
1792
|
+
io.stdout(`State: approved\n`);
|
|
1793
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1794
|
+
io.stdout(`Regions: ${result.regionCount}\n`);
|
|
1795
|
+
io.stdout(`Requirements: ${result.requirementCount}\n`);
|
|
1796
|
+
io.stdout(`Adequacy: ${result.adequacy.status}\n`);
|
|
1797
|
+
io.stdout(`Applicability: ${result.hasApplicability ? 'declared' : 'none'}\n`);
|
|
1798
|
+
return 0;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Thin orchestration only: parse args, load the optional bindings file
|
|
1802
|
+
* (syntax/root-shape only - every binding-declaration rule stays owned by
|
|
1803
|
+
* `isValidReferenceRuntimeBindingDeclarations`, called inside the domain
|
|
1804
|
+
* evaluator), then delegate to the existing
|
|
1805
|
+
* `evaluateReferenceCandidateFidelityFromArtifactRoots` application function
|
|
1806
|
+
* exactly once. No adequacy/compatibility/binding/tolerance/coordinate-
|
|
1807
|
+
* mapping/relationship logic lives here - see
|
|
1808
|
+
* `src/domain/externalReferenceFidelity.ts`. `--enforce` is applied only
|
|
1809
|
+
* after the evaluation has already been computed: it selects the process
|
|
1810
|
+
* exit status for an already-final "fail" fidelity state and never affects
|
|
1811
|
+
* the evaluation's content. Persists nothing.
|
|
1812
|
+
*/
|
|
1813
|
+
async function runEvaluateReferenceFidelityCommand(argv, io) {
|
|
1814
|
+
if (argv.includes('--help')) {
|
|
1815
|
+
io.stdout(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
1816
|
+
return 0;
|
|
1817
|
+
}
|
|
1818
|
+
const parsedArgs = parseEvaluateReferenceFidelityArgs(argv);
|
|
1819
|
+
if (!parsedArgs.ok) {
|
|
1820
|
+
for (const error of parsedArgs.errors)
|
|
1821
|
+
io.stderr(`error: ${error}\n`);
|
|
1822
|
+
io.stderr(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
1823
|
+
return 1;
|
|
1824
|
+
}
|
|
1825
|
+
let bindings = [];
|
|
1826
|
+
if (parsedArgs.bindingsFilePath !== undefined) {
|
|
1827
|
+
const loaded = loadBindingsFile(parsedArgs.bindingsFilePath);
|
|
1828
|
+
if (!loaded.ok) {
|
|
1829
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1830
|
+
io.stderr(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
1831
|
+
return 1;
|
|
1832
|
+
}
|
|
1833
|
+
// CLI boundary owns file-read/root-wrapper syntax only; binding-declaration shape/bounds/existence/conflict validation is owned by isValidReferenceRuntimeBindingDeclarations, called inside evaluateReferenceCandidateFidelity.
|
|
1834
|
+
bindings = loaded.bindings;
|
|
1835
|
+
}
|
|
1836
|
+
// Exactly one application evaluation attempt: reads reference/candidate once each, calls the canonical evaluator exactly once.
|
|
1837
|
+
const result = await evaluateReferenceCandidateFidelityFromArtifactRoots(parsedArgs.referenceRoot, parsedArgs.candidateRoot, bindings);
|
|
1838
|
+
if (!result.ok) {
|
|
1839
|
+
for (const diagnostic of result.diagnostics)
|
|
1840
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1841
|
+
return 1;
|
|
1842
|
+
}
|
|
1843
|
+
const { evaluation } = result;
|
|
1844
|
+
const passCount = evaluation.requirementResults.filter((r) => r.status === 'pass').length;
|
|
1845
|
+
const failCount = evaluation.requirementResults.filter((r) => r.status === 'fail').length;
|
|
1846
|
+
const unavailableCount = evaluation.requirementResults.filter((r) => r.status === 'unavailable').length;
|
|
1847
|
+
io.stdout(`Reference: ${evaluation.referenceId}\n`);
|
|
1848
|
+
io.stdout(`Candidate: ${evaluation.candidateObservationId}\n`);
|
|
1849
|
+
io.stdout(`Adequacy: ${evaluation.adequacy.status}\n`);
|
|
1850
|
+
if (evaluation.compatibility !== undefined)
|
|
1851
|
+
io.stdout(`Compatibility: ${evaluation.compatibility.state}\n`);
|
|
1852
|
+
io.stdout(`State: ${evaluation.state}\n`);
|
|
1853
|
+
if (evaluation.blockedBy !== undefined)
|
|
1854
|
+
io.stdout(`Blocked by: ${evaluation.blockedBy}\n`);
|
|
1855
|
+
io.stdout(`Requirements: ${evaluation.requirementResults.length} (pass: ${passCount}, fail: ${failCount}, unavailable: ${unavailableCount})\n`);
|
|
1856
|
+
io.stdout(`Enforced: ${parsedArgs.enforce ? 'yes' : 'no'}\n`);
|
|
1857
|
+
if (parsedArgs.enforce && evaluation.state === 'fail')
|
|
1858
|
+
return 1;
|
|
1859
|
+
return 0;
|
|
1860
|
+
}
|
|
1026
1861
|
/** Testable CLI entry point: pure function of argv (+ injectable IO), no direct process.exit. */
|
|
1027
1862
|
export async function runCli(argv, io = defaultIO) {
|
|
1028
1863
|
const [command, ...rest] = argv;
|
|
@@ -1053,6 +1888,15 @@ export async function runCli(argv, io = defaultIO) {
|
|
|
1053
1888
|
if (command === 'evaluate-contract') {
|
|
1054
1889
|
return runEvaluateContractCommand(rest, io);
|
|
1055
1890
|
}
|
|
1891
|
+
if (command === 'import-reference') {
|
|
1892
|
+
return runImportReferenceCommand(rest, io);
|
|
1893
|
+
}
|
|
1894
|
+
if (command === 'approve-reference') {
|
|
1895
|
+
return runApproveReferenceCommand(rest, io);
|
|
1896
|
+
}
|
|
1897
|
+
if (command === 'evaluate-reference-fidelity') {
|
|
1898
|
+
return runEvaluateReferenceFidelityCommand(rest, io);
|
|
1899
|
+
}
|
|
1056
1900
|
io.stderr(`error: unrecognized command "${command}"\n`);
|
|
1057
1901
|
io.stderr(TOP_LEVEL_HELP);
|
|
1058
1902
|
return 1;
|