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.
Files changed (118) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +43 -4
  3. package/dist/application/externalReferencePersistenceService.d.ts +75 -0
  4. package/dist/application/externalReferencePersistenceService.js +182 -0
  5. package/dist/application/externalReferencePersistenceService.js.map +1 -0
  6. package/dist/application/referenceFidelityEvaluationService.d.ts +28 -0
  7. package/dist/application/referenceFidelityEvaluationService.js +45 -0
  8. package/dist/application/referenceFidelityEvaluationService.js.map +1 -0
  9. package/dist/artifacts/externalReferenceArtifactReader.d.ts +18 -0
  10. package/dist/artifacts/externalReferenceArtifactReader.js +35 -0
  11. package/dist/artifacts/externalReferenceArtifactReader.js.map +1 -0
  12. package/dist/artifacts/externalReferenceArtifactWriter.d.ts +44 -0
  13. package/dist/artifacts/externalReferenceArtifactWriter.js +77 -0
  14. package/dist/artifacts/externalReferenceArtifactWriter.js.map +1 -0
  15. package/dist/cli.js +844 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/domain/boundedAgentContext.d.ts +52 -0
  18. package/dist/domain/boundedAgentContext.js +44 -1
  19. package/dist/domain/boundedAgentContext.js.map +1 -1
  20. package/dist/domain/boundedAgentContextIdentity.d.ts +16 -6
  21. package/dist/domain/boundedAgentContextIdentity.js +22 -6
  22. package/dist/domain/boundedAgentContextIdentity.js.map +1 -1
  23. package/dist/domain/boundedAgentContextProjection.d.ts +11 -0
  24. package/dist/domain/boundedAgentContextProjection.js +32 -4
  25. package/dist/domain/boundedAgentContextProjection.js.map +1 -1
  26. package/dist/domain/comparison.d.ts +23 -1
  27. package/dist/domain/comparison.js +27 -1
  28. package/dist/domain/comparison.js.map +1 -1
  29. package/dist/domain/comparisonEngine.d.ts +34 -6
  30. package/dist/domain/comparisonEngine.js +48 -8
  31. package/dist/domain/comparisonEngine.js.map +1 -1
  32. package/dist/domain/diagnostics.d.ts +1 -1
  33. package/dist/domain/diagnostics.js +16 -0
  34. package/dist/domain/diagnostics.js.map +1 -1
  35. package/dist/domain/explicitState.d.ts +40 -0
  36. package/dist/domain/explicitState.js +54 -0
  37. package/dist/domain/explicitState.js.map +1 -0
  38. package/dist/domain/externalReference.d.ts +158 -0
  39. package/dist/domain/externalReference.js +167 -0
  40. package/dist/domain/externalReference.js.map +1 -0
  41. package/dist/domain/externalReferenceApplicability.d.ts +43 -0
  42. package/dist/domain/externalReferenceApplicability.js +52 -0
  43. package/dist/domain/externalReferenceApplicability.js.map +1 -0
  44. package/dist/domain/externalReferenceCompatibility.d.ts +40 -0
  45. package/dist/domain/externalReferenceCompatibility.js +48 -0
  46. package/dist/domain/externalReferenceCompatibility.js.map +1 -0
  47. package/dist/domain/externalReferenceFidelity.d.ts +141 -0
  48. package/dist/domain/externalReferenceFidelity.js +413 -0
  49. package/dist/domain/externalReferenceFidelity.js.map +1 -0
  50. package/dist/domain/externalReferenceIdentity.d.ts +38 -0
  51. package/dist/domain/externalReferenceIdentity.js +70 -0
  52. package/dist/domain/externalReferenceIdentity.js.map +1 -0
  53. package/dist/domain/externalReferenceImage.d.ts +35 -0
  54. package/dist/domain/externalReferenceImage.js +160 -0
  55. package/dist/domain/externalReferenceImage.js.map +1 -0
  56. package/dist/domain/externalReferenceRegionRelationships.d.ts +63 -0
  57. package/dist/domain/externalReferenceRegionRelationships.js +98 -0
  58. package/dist/domain/externalReferenceRegionRelationships.js.map +1 -0
  59. package/dist/domain/externalReferenceRegions.d.ts +65 -0
  60. package/dist/domain/externalReferenceRegions.js +105 -0
  61. package/dist/domain/externalReferenceRegions.js.map +1 -0
  62. package/dist/domain/externalReferenceRequirementIdentity.d.ts +12 -0
  63. package/dist/domain/externalReferenceRequirementIdentity.js +35 -0
  64. package/dist/domain/externalReferenceRequirementIdentity.js.map +1 -0
  65. package/dist/domain/externalReferenceRequirements.d.ts +215 -0
  66. package/dist/domain/externalReferenceRequirements.js +401 -0
  67. package/dist/domain/externalReferenceRequirements.js.map +1 -0
  68. package/dist/domain/externalReferenceRuntimeBinding.d.ts +146 -0
  69. package/dist/domain/externalReferenceRuntimeBinding.js +183 -0
  70. package/dist/domain/externalReferenceRuntimeBinding.js.map +1 -0
  71. package/dist/domain/identity.d.ts +11 -8
  72. package/dist/domain/identity.js +12 -8
  73. package/dist/domain/identity.js.map +1 -1
  74. package/dist/domain/referenceCorrectionIdentity.d.ts +40 -0
  75. package/dist/domain/referenceCorrectionIdentity.js +77 -0
  76. package/dist/domain/referenceCorrectionIdentity.js.map +1 -0
  77. package/dist/domain/referenceCorrectionWorkflow.d.ts +160 -0
  78. package/dist/domain/referenceCorrectionWorkflow.js +165 -0
  79. package/dist/domain/referenceCorrectionWorkflow.js.map +1 -0
  80. package/dist/domain/referenceFidelityProjection.d.ts +65 -0
  81. package/dist/domain/referenceFidelityProjection.js +135 -0
  82. package/dist/domain/referenceFidelityProjection.js.map +1 -0
  83. package/dist/domain/relationships.d.ts +43 -1
  84. package/dist/domain/relationships.js +15 -6
  85. package/dist/domain/relationships.js.map +1 -1
  86. package/dist/domain/schema.js +6 -0
  87. package/dist/domain/schema.js.map +1 -1
  88. package/dist/index.d.ts +39 -4
  89. package/dist/index.js +21 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/request/request.d.ts +12 -0
  92. package/dist/request/request.js +12 -0
  93. package/dist/request/request.js.map +1 -1
  94. package/docs/ARCHITECTURE.md +271 -11
  95. package/docs/CI_CD.md +50 -0
  96. package/docs/COMMANDS.md +187 -0
  97. package/docs/CONTRACTS.md +1347 -12
  98. package/docs/CURRENT_STATE.md +451 -13
  99. package/docs/PROJECT_DESCRIPTION.md +513 -88
  100. package/docs/PROJECT_MILESTONES.md +550 -97
  101. package/docs/PROJECT_OVERVIEW.md +56 -14
  102. package/docs/QUICKSTART.md +6 -0
  103. package/docs/RELEASE.md +28 -21
  104. package/docs/ROADMAP.md +238 -99
  105. package/docs/SECURITY.md +80 -4
  106. package/docs/WORKFLOWS.md +358 -23
  107. package/docs/reports/v0.7-bounded-fidelity-context-prompt7.md +243 -0
  108. package/docs/reports/v0.7-implementation-completeness-documentation-reconciliation.md +497 -0
  109. package/docs/reports/v0.7-pre-release-readiness.md +337 -0
  110. package/docs/reports/v0.7-reference-binding-prompt5.md +223 -0
  111. package/docs/reports/v0.7-reference-compatibility-prompt4.md +234 -0
  112. package/docs/reports/v0.7-reference-correction-workflow-prompt8.md +222 -0
  113. package/docs/reports/v0.7-reference-fidelity-prompt6.md +216 -0
  114. package/docs/reports/v0.7-reference-foundation-prompt1.md +151 -0
  115. package/docs/reports/v0.7-reference-regions-prompt2.md +195 -0
  116. package/docs/reports/v0.7-reference-requirements-prompt3.md +217 -0
  117. package/docs/reports/v0.7-release-prep.md +423 -0
  118. 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;