@dailephd/my-frontend-observer 0.8.1
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 +397 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/application/browserCaptureService.d.ts +11 -0
- package/dist/application/browserCaptureService.js +12 -0
- package/dist/application/browserCaptureService.js.map +1 -0
- package/dist/application/comparisonService.d.ts +56 -0
- package/dist/application/comparisonService.js +77 -0
- package/dist/application/comparisonService.js.map +1 -0
- package/dist/application/externalReferencePersistenceService.d.ts +75 -0
- package/dist/application/externalReferencePersistenceService.js +182 -0
- package/dist/application/externalReferencePersistenceService.js.map +1 -0
- package/dist/application/frontendContractEvaluationService.d.ts +49 -0
- package/dist/application/frontendContractEvaluationService.js +112 -0
- package/dist/application/frontendContractEvaluationService.js.map +1 -0
- package/dist/application/frontendContractPersistenceService.d.ts +56 -0
- package/dist/application/frontendContractPersistenceService.js +91 -0
- package/dist/application/frontendContractPersistenceService.js.map +1 -0
- package/dist/application/observationPersistence.d.ts +59 -0
- package/dist/application/observationPersistence.js +79 -0
- package/dist/application/observationPersistence.js.map +1 -0
- package/dist/application/projectCheckService.d.ts +3 -0
- package/dist/application/projectCheckService.js +169 -0
- package/dist/application/projectCheckService.js.map +1 -0
- package/dist/application/projectWorkflowService.d.ts +46 -0
- package/dist/application/projectWorkflowService.js +92 -0
- package/dist/application/projectWorkflowService.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/artifactReader.d.ts +19 -0
- package/dist/artifacts/artifactReader.js +36 -0
- package/dist/artifacts/artifactReader.js.map +1 -0
- package/dist/artifacts/artifactWriter.d.ts +25 -0
- package/dist/artifacts/artifactWriter.js +68 -0
- package/dist/artifacts/artifactWriter.js.map +1 -0
- package/dist/artifacts/comparisonArtifactReader.d.ts +18 -0
- package/dist/artifacts/comparisonArtifactReader.js +35 -0
- package/dist/artifacts/comparisonArtifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
- package/dist/artifacts/comparisonArtifactWriter.js +67 -0
- package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
- package/dist/artifacts/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/artifacts/frontendContractArtifactReader.d.ts +24 -0
- package/dist/artifacts/frontendContractArtifactReader.js +47 -0
- package/dist/artifacts/frontendContractArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractArtifactWriter.d.ts +34 -0
- package/dist/artifacts/frontendContractArtifactWriter.js +70 -0
- package/dist/artifacts/frontendContractArtifactWriter.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.d.ts +17 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js +34 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.d.ts +32 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js +58 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js.map +1 -0
- package/dist/artifacts/types.d.ts +17 -0
- package/dist/artifacts/types.js +2 -0
- package/dist/artifacts/types.js.map +1 -0
- package/dist/browser/chromiumAdapter.d.ts +21 -0
- package/dist/browser/chromiumAdapter.js +204 -0
- package/dist/browser/chromiumAdapter.js.map +1 -0
- package/dist/browser/evidenceCapture.d.ts +62 -0
- package/dist/browser/evidenceCapture.js +500 -0
- package/dist/browser/evidenceCapture.js.map +1 -0
- package/dist/browser/scrollCapture.d.ts +32 -0
- package/dist/browser/scrollCapture.js +163 -0
- package/dist/browser/scrollCapture.js.map +1 -0
- package/dist/browser/types.d.ts +24 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +2411 -0
- package/dist/cli.js.map +1 -0
- package/dist/domain/boundedAgentContext.d.ts +226 -0
- package/dist/domain/boundedAgentContext.js +355 -0
- package/dist/domain/boundedAgentContext.js.map +1 -0
- package/dist/domain/boundedAgentContextCorrelation.d.ts +74 -0
- package/dist/domain/boundedAgentContextCorrelation.js +441 -0
- package/dist/domain/boundedAgentContextCorrelation.js.map +1 -0
- package/dist/domain/boundedAgentContextIdentity.d.ts +26 -0
- package/dist/domain/boundedAgentContextIdentity.js +69 -0
- package/dist/domain/boundedAgentContextIdentity.js.map +1 -0
- package/dist/domain/boundedAgentContextProjection.d.ts +71 -0
- package/dist/domain/boundedAgentContextProjection.js +477 -0
- package/dist/domain/boundedAgentContextProjection.js.map +1 -0
- package/dist/domain/comparison.d.ts +220 -0
- package/dist/domain/comparison.js +350 -0
- package/dist/domain/comparison.js.map +1 -0
- package/dist/domain/comparisonEngine.d.ts +76 -0
- package/dist/domain/comparisonEngine.js +734 -0
- package/dist/domain/comparisonEngine.js.map +1 -0
- package/dist/domain/comparisonIdentity.d.ts +13 -0
- package/dist/domain/comparisonIdentity.js +46 -0
- package/dist/domain/comparisonIdentity.js.map +1 -0
- package/dist/domain/completion.d.ts +30 -0
- package/dist/domain/completion.js +22 -0
- package/dist/domain/completion.js.map +1 -0
- package/dist/domain/diagnostics.d.ts +17 -0
- package/dist/domain/diagnostics.js +77 -0
- package/dist/domain/diagnostics.js.map +1 -0
- package/dist/domain/evidence.d.ts +29 -0
- package/dist/domain/evidence.js +59 -0
- package/dist/domain/evidence.js.map +1 -0
- 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 +171 -0
- package/dist/domain/externalReferenceFidelity.js +419 -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/frontendContractEvaluation.d.ts +57 -0
- package/dist/domain/frontendContractEvaluation.js +454 -0
- package/dist/domain/frontendContractEvaluation.js.map +1 -0
- package/dist/domain/frontendContractEvaluationArtifact.d.ts +65 -0
- package/dist/domain/frontendContractEvaluationArtifact.js +108 -0
- package/dist/domain/frontendContractEvaluationArtifact.js.map +1 -0
- package/dist/domain/frontendContractIdentity.d.ts +39 -0
- package/dist/domain/frontendContractIdentity.js +70 -0
- package/dist/domain/frontendContractIdentity.js.map +1 -0
- package/dist/domain/frontendContracts.d.ts +188 -0
- package/dist/domain/frontendContracts.js +260 -0
- package/dist/domain/frontendContracts.js.map +1 -0
- package/dist/domain/identity.d.ts +21 -0
- package/dist/domain/identity.js +47 -0
- package/dist/domain/identity.js.map +1 -0
- 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 +210 -0
- package/dist/domain/relationships.js +352 -0
- package/dist/domain/relationships.js.map +1 -0
- package/dist/domain/schema.d.ts +269 -0
- package/dist/domain/schema.js +442 -0
- package/dist/domain/schema.js.map +1 -0
- package/dist/domain/scrollEvidence.d.ts +51 -0
- package/dist/domain/scrollEvidence.js +134 -0
- package/dist/domain/scrollEvidence.js.map +1 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/projectWorkflow/aliasCatalog.d.ts +21 -0
- package/dist/projectWorkflow/aliasCatalog.js +67 -0
- package/dist/projectWorkflow/aliasCatalog.js.map +1 -0
- package/dist/projectWorkflow/checkAcceptance.d.ts +25 -0
- package/dist/projectWorkflow/checkAcceptance.js +55 -0
- package/dist/projectWorkflow/checkAcceptance.js.map +1 -0
- package/dist/projectWorkflow/checkResult.d.ts +85 -0
- package/dist/projectWorkflow/checkResult.js +101 -0
- package/dist/projectWorkflow/checkResult.js.map +1 -0
- package/dist/projectWorkflow/projectConfig.d.ts +43 -0
- package/dist/projectWorkflow/projectConfig.js +84 -0
- package/dist/projectWorkflow/projectConfig.js.map +1 -0
- package/dist/projectWorkflow/projectDiscovery.d.ts +9 -0
- package/dist/projectWorkflow/projectDiscovery.js +20 -0
- package/dist/projectWorkflow/projectDiscovery.js.map +1 -0
- package/dist/projectWorkflow/projectPaths.d.ts +8 -0
- package/dist/projectWorkflow/projectPaths.js +23 -0
- package/dist/projectWorkflow/projectPaths.js.map +1 -0
- package/dist/request/paths.d.ts +14 -0
- package/dist/request/paths.js +33 -0
- package/dist/request/paths.js.map +1 -0
- package/dist/request/request.d.ts +111 -0
- package/dist/request/request.js +464 -0
- package/dist/request/request.js.map +1 -0
- package/dist/safety/policy.d.ts +14 -0
- package/dist/safety/policy.js +81 -0
- package/dist/safety/policy.js.map +1 -0
- package/dist/viewer/assets/index-CN_yb9Uf.css +1 -0
- package/dist/viewer/assets/index-D98S1_2d.js +9 -0
- package/dist/viewer/icons/icon-192.png +0 -0
- package/dist/viewer/icons/icon-512.png +0 -0
- package/dist/viewer/index.html +15 -0
- package/dist/viewer/manifest.webmanifest +1 -0
- package/dist/viewer/registerSW.js +1 -0
- package/dist/viewer/sw.js +1 -0
- package/dist/viewer/workbox-9c191d2f.js +1 -0
- package/dist/viewerServer/context.d.ts +48 -0
- package/dist/viewerServer/context.js +60 -0
- package/dist/viewerServer/context.js.map +1 -0
- package/dist/viewerServer/evidence/classify.d.ts +59 -0
- package/dist/viewerServer/evidence/classify.js +124 -0
- package/dist/viewerServer/evidence/classify.js.map +1 -0
- package/dist/viewerServer/evidence/comparisonView.d.ts +28 -0
- package/dist/viewerServer/evidence/comparisonView.js +43 -0
- package/dist/viewerServer/evidence/comparisonView.js.map +1 -0
- package/dist/viewerServer/evidence/contextSourceView.d.ts +25 -0
- package/dist/viewerServer/evidence/contextSourceView.js +20 -0
- package/dist/viewerServer/evidence/contextSourceView.js.map +1 -0
- package/dist/viewerServer/evidence/discovery.d.ts +31 -0
- package/dist/viewerServer/evidence/discovery.js +78 -0
- package/dist/viewerServer/evidence/discovery.js.map +1 -0
- package/dist/viewerServer/evidence/evaluationView.d.ts +32 -0
- package/dist/viewerServer/evidence/evaluationView.js +50 -0
- package/dist/viewerServer/evidence/evaluationView.js.map +1 -0
- package/dist/viewerServer/evidence/handles.d.ts +21 -0
- package/dist/viewerServer/evidence/handles.js +43 -0
- package/dist/viewerServer/evidence/handles.js.map +1 -0
- package/dist/viewerServer/evidence/index.d.ts +41 -0
- package/dist/viewerServer/evidence/index.js +82 -0
- package/dist/viewerServer/evidence/index.js.map +1 -0
- package/dist/viewerServer/evidence/limits.d.ts +27 -0
- package/dist/viewerServer/evidence/limits.js +28 -0
- package/dist/viewerServer/evidence/limits.js.map +1 -0
- package/dist/viewerServer/evidence/linkedEvidence.d.ts +43 -0
- package/dist/viewerServer/evidence/linkedEvidence.js +151 -0
- package/dist/viewerServer/evidence/linkedEvidence.js.map +1 -0
- package/dist/viewerServer/evidence/mediaResolver.d.ts +16 -0
- package/dist/viewerServer/evidence/mediaResolver.js +85 -0
- package/dist/viewerServer/evidence/mediaResolver.js.map +1 -0
- package/dist/viewerServer/evidence/observationView.d.ts +29 -0
- package/dist/viewerServer/evidence/observationView.js +46 -0
- package/dist/viewerServer/evidence/observationView.js.map +1 -0
- package/dist/viewerServer/evidence/pathSafety.d.ts +11 -0
- package/dist/viewerServer/evidence/pathSafety.js +31 -0
- package/dist/viewerServer/evidence/pathSafety.js.map +1 -0
- package/dist/viewerServer/evidence/projection.d.ts +55 -0
- package/dist/viewerServer/evidence/projection.js +152 -0
- package/dist/viewerServer/evidence/projection.js.map +1 -0
- package/dist/viewerServer/evidence/referenceView.d.ts +133 -0
- package/dist/viewerServer/evidence/referenceView.js +169 -0
- package/dist/viewerServer/evidence/referenceView.js.map +1 -0
- package/dist/viewerServer/httpServer.d.ts +27 -0
- package/dist/viewerServer/httpServer.js +381 -0
- package/dist/viewerServer/httpServer.js.map +1 -0
- package/dist/viewerServer/openBrowser.d.ts +7 -0
- package/dist/viewerServer/openBrowser.js +32 -0
- package/dist/viewerServer/openBrowser.js.map +1 -0
- package/dist/viewerServer/port.d.ts +16 -0
- package/dist/viewerServer/port.js +19 -0
- package/dist/viewerServer/port.js.map +1 -0
- package/dist/viewerServer/viewerService.d.ts +62 -0
- package/dist/viewerServer/viewerService.js +88 -0
- package/dist/viewerServer/viewerService.js.map +1 -0
- package/docs/ARCHITECTURE.md +1286 -0
- package/docs/CI_CD.md +250 -0
- package/docs/COMMANDS.md +972 -0
- package/docs/CONTRACTS.md +1856 -0
- package/docs/CURRENT_STATE.md +1049 -0
- package/docs/DEVELOPMENT.md +202 -0
- package/docs/DOCUMENTATION_PRESERVATION_POLICY.md +50 -0
- package/docs/PROJECT_DESCRIPTION.md +2221 -0
- package/docs/PROJECT_MILESTONES.md +2526 -0
- package/docs/PROJECT_OVERVIEW.md +150 -0
- package/docs/QUICKSTART.md +83 -0
- package/docs/RELEASE.md +27 -0
- package/docs/ROADMAP.md +641 -0
- package/docs/SECURITY.md +218 -0
- package/docs/WORKFLOWS.md +642 -0
- package/docs/plans/v0.8-implementation-plan.md +655 -0
- package/docs/plans/v0.8.1-cli-usability-patch-plan.md +505 -0
- 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/docs/reports/v0.8-binding-fidelity-interaction-batch6.md +279 -0
- package/docs/reports/v0.8-bounded-context-correlation-batch7.md +233 -0
- package/docs/reports/v0.8-comparison-contract-inspection-batch4.md +279 -0
- package/docs/reports/v0.8-evidence-index-readers-batch2.md +247 -0
- package/docs/reports/v0.8-implementation-completeness-documentation-reconciliation.md +741 -0
- package/docs/reports/v0.8-integrated-viewer-acceptance-batch8.md +128 -0
- package/docs/reports/v0.8-observation-svg-inspection-batch3.md +223 -0
- package/docs/reports/v0.8-prerelease-readiness-cross-platform-security-code-rot.md +687 -0
- package/docs/reports/v0.8-reference-candidate-inspection-batch5.md +232 -0
- package/docs/reports/v0.8-viewer-runtime-pwa-batch1.md +278 -0
- package/docs/reports/v0.8.1-check-orchestration-prompt2.md +69 -0
- package/docs/reports/v0.8.1-implementation-completeness-documentation-reconciliation.md +114 -0
- package/docs/reports/v0.8.1-prerelease-readiness-cross-platform-security-code-rot.md +170 -0
- package/docs/reports/v0.8.1-project-workflow-foundation-prompt1.md +66 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { normalizeRequest } from './request/request.js';
|
|
5
|
+
import { getProducerInfo } from './domain/schema.js';
|
|
6
|
+
import { observe } from './application/observationPersistence.js';
|
|
7
|
+
import { compareAndPersistFromArtifactRoots } from './application/comparisonService.js';
|
|
8
|
+
import { approveAndPersistBaseline, persistPerChangeContract } from './application/frontendContractPersistenceService.js';
|
|
9
|
+
import { evaluateAndPersistFromArtifactRoots } from './application/frontendContractEvaluationService.js';
|
|
10
|
+
import { importExternalReference, approveExternalReference } from './application/externalReferencePersistenceService.js';
|
|
11
|
+
import { evaluateReferenceCandidateFidelityFromArtifactRoots } from './application/referenceFidelityEvaluationService.js';
|
|
12
|
+
import { startViewer } from './viewerServer/viewerService.js';
|
|
13
|
+
import { openInDefaultBrowser } from './viewerServer/openBrowser.js';
|
|
14
|
+
import { DEFAULT_VIEWER_PORT } from './viewerServer/port.js';
|
|
15
|
+
import { classifyContextFileContent, MAX_CONTEXT_FILE_BYTES } from './viewerServer/context.js';
|
|
16
|
+
import { discoverFrontendObserverProject } from './projectWorkflow/projectDiscovery.js';
|
|
17
|
+
import { captureNamedObservation, initializeFrontendObserverProject, loadProjectViewerState } from './application/projectWorkflowService.js';
|
|
18
|
+
import { checkProject } from './application/projectCheckService.js';
|
|
19
|
+
import { loadBindingsFile } from './projectWorkflow/checkAcceptance.js';
|
|
20
|
+
const defaultIO = {
|
|
21
|
+
stdout: (text) => {
|
|
22
|
+
process.stdout.write(text);
|
|
23
|
+
},
|
|
24
|
+
stderr: (text) => {
|
|
25
|
+
process.stderr.write(text);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const TOP_LEVEL_HELP = `my-frontend-observer - local-first browser runtime evidence producer
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
my-frontend-observer <command> [options]
|
|
32
|
+
|
|
33
|
+
Common workflow:
|
|
34
|
+
init Initialize project-local Observer configuration and state.
|
|
35
|
+
capture <alias> Capture one project-configured observation under a human alias.
|
|
36
|
+
check [baseline] Capture current state and evaluate configured acceptance.
|
|
37
|
+
view Start the viewer; discovers project evidence when --root is omitted.
|
|
38
|
+
|
|
39
|
+
Advanced:
|
|
40
|
+
observe Capture one bounded browser observation and persist
|
|
41
|
+
it as a portable artifact.
|
|
42
|
+
compare Compare two persisted observations and write a
|
|
43
|
+
structured comparison artifact.
|
|
44
|
+
approve-baseline Explicitly approve and persist one already-authored
|
|
45
|
+
persistent baseline contract against the observation
|
|
46
|
+
it claims to approve.
|
|
47
|
+
save-change-contract Validate and persist one already-authored per-change
|
|
48
|
+
contract so it can later be evaluated.
|
|
49
|
+
evaluate-contract Evaluate a candidate change against an approved
|
|
50
|
+
baseline, a per-change contract, and existing
|
|
51
|
+
before/after/comparison evidence, and persist the
|
|
52
|
+
result.
|
|
53
|
+
import-reference Validate and persist one local external design-
|
|
54
|
+
reference image as a new, unapproved
|
|
55
|
+
external-reference artifact.
|
|
56
|
+
approve-reference Explicitly approve one already-imported
|
|
57
|
+
external-reference artifact, persisting a new
|
|
58
|
+
approved artifact instance.
|
|
59
|
+
evaluate-reference-fidelity Evaluate whether a candidate observation
|
|
60
|
+
satisfies an external reference's selected design
|
|
61
|
+
requirements, gated by reference adequacy,
|
|
62
|
+
reference/candidate compatibility, and explicit
|
|
63
|
+
region-to-target bindings. Prints a structured
|
|
64
|
+
result; persists nothing.
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--help Show this help.
|
|
68
|
+
--version Print the package version.
|
|
69
|
+
|
|
70
|
+
Run "my-frontend-observer <command> --help" for command-specific options.
|
|
71
|
+
`;
|
|
72
|
+
const VIEW_HELP = `Usage:
|
|
73
|
+
my-frontend-observer view [--root <evidence-root>] [--bindings-file <json-file>] [--context-file <json-file>] [options]
|
|
74
|
+
|
|
75
|
+
Project or standalone input:
|
|
76
|
+
--root <path> Local evidence-root directory the viewer session
|
|
77
|
+
represents. When supplied, existing standalone behavior is
|
|
78
|
+
used and no initialized project or alias catalog is required.
|
|
79
|
+
Without --root, view requires an initialized project and
|
|
80
|
+
discovers its managed evidence root and alias metadata.
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--port <n> TCP port to bind, in [0, 65535]. Defaults to ${DEFAULT_VIEWER_PORT}.
|
|
84
|
+
An explicit alternate port is a different web origin than
|
|
85
|
+
the default - an installed PWA is not portable across
|
|
86
|
+
origins. If the requested port is already in use, this
|
|
87
|
+
command fails with an actionable error; it never silently
|
|
88
|
+
falls back to a different port.
|
|
89
|
+
--bindings-file <json-file> Local JSON file of the form
|
|
90
|
+
{ "bindings": [ { "referenceRegion": "...", "runtimeTarget": "..." } ] }
|
|
91
|
+
(the exact same operational wrapper format as
|
|
92
|
+
\`evaluate-reference-fidelity --bindings-file\`, sharing its
|
|
93
|
+
parser). Explicit, session-only viewer input: read once at
|
|
94
|
+
startup, never persisted, never written into any Observer
|
|
95
|
+
artifact, and never exposed as a path to the browser. Its
|
|
96
|
+
declarations become available for on-demand binding/
|
|
97
|
+
fidelity evaluation once a reference and candidate are
|
|
98
|
+
explicitly selected in the viewer. Reference-specific
|
|
99
|
+
validity (region existence, etc.) is checked when a
|
|
100
|
+
reference is actually selected, not at startup - only the
|
|
101
|
+
file's own readability/JSON/wrapper shape is validated at
|
|
102
|
+
startup. Omit to run with no binding declarations (the
|
|
103
|
+
viewer remains fully usable; cross-selection stays
|
|
104
|
+
disabled).
|
|
105
|
+
--context-file <json-file> Local JSON file containing exactly one
|
|
106
|
+
BoundedAgentContextArtifact value directly (no wrapper
|
|
107
|
+
object) - e.g. { "artifactKind":
|
|
108
|
+
"my-frontend-observer/bounded-agent-context",
|
|
109
|
+
"schemaVersion": "1.0.0", ... }. Explicit, session-only
|
|
110
|
+
viewer input: read once at startup, validated through the
|
|
111
|
+
existing canonical isValidBoundedAgentContextArtifact,
|
|
112
|
+
held only in server memory, never persisted, never written
|
|
113
|
+
into any Observer artifact, and never exposed as a path to
|
|
114
|
+
the browser. Bounded agent context remains programmatic-
|
|
115
|
+
only as an Observer-produced contract - this command does
|
|
116
|
+
not add a way to generate, save, or write one; the viewer
|
|
117
|
+
never rebuilds it (no projectBoundedAgentContext call) and
|
|
118
|
+
never derives runtime/static correlation (no
|
|
119
|
+
deriveRuntimeStaticCorrelations/attachRuntimeStaticCorrelations
|
|
120
|
+
call) - it only displays the exact context it was given.
|
|
121
|
+
A recognized artifactKind with a schemaVersion other than
|
|
122
|
+
the currently supported one starts the viewer showing an
|
|
123
|
+
honest "unsupported version" context state rather than
|
|
124
|
+
failing. An unreadable file, invalid JSON, wrong
|
|
125
|
+
artifactKind, or a structurally invalid current-schema
|
|
126
|
+
artifact fails startup clearly. Omit to run with no
|
|
127
|
+
bounded context supplied (the viewer remains fully usable;
|
|
128
|
+
the context panel says none was supplied). May be combined
|
|
129
|
+
with --bindings-file.
|
|
130
|
+
--no-open Do not attempt to open the system default browser after
|
|
131
|
+
the server starts. Browser auto-open is a best-effort
|
|
132
|
+
convenience only: its failure is never fatal and never
|
|
133
|
+
affects server startup success.
|
|
134
|
+
--help Show this help.
|
|
135
|
+
|
|
136
|
+
Starts one Node HTTP server bound only to 127.0.0.1, serving the built
|
|
137
|
+
React + TypeScript + Vite viewer application (and its PWA manifest/service
|
|
138
|
+
worker) plus one minimal read-only status endpoint. The server never writes
|
|
139
|
+
to the supplied evidence root, never exposes it as a generic static
|
|
140
|
+
directory, and never launches a browser observation. The process keeps
|
|
141
|
+
running (serving the viewer) until interrupted. On success, prints the
|
|
142
|
+
viewer URL and exits only when the server stops. On invalid syntax, a
|
|
143
|
+
missing/non-directory --root, an invalid --port, or a port already in use,
|
|
144
|
+
prints structured diagnostics to stderr and exits nonzero without starting
|
|
145
|
+
a server.
|
|
146
|
+
`;
|
|
147
|
+
const INIT_HELP = `Usage:
|
|
148
|
+
my-frontend-observer init --url <loopback-url> [options]
|
|
149
|
+
|
|
150
|
+
Options:
|
|
151
|
+
--url <loopback-url> Required loopback frontend URL.
|
|
152
|
+
--viewport <WIDTHxHEIGHT> Viewport size, e.g. 1280x720.
|
|
153
|
+
--target <id=selector> Repeatable CSS-shorthand target.
|
|
154
|
+
--targets-file <json-file> Structured target file; mutually exclusive with --target.
|
|
155
|
+
--default-baseline <alias> Default baseline alias. Defaults to baseline.
|
|
156
|
+
--replace Replace configuration only; preserve managed evidence/catalog.
|
|
157
|
+
--help Show this help.
|
|
158
|
+
`;
|
|
159
|
+
const CAPTURE_HELP = `Usage:
|
|
160
|
+
my-frontend-observer capture <alias> [--replace]
|
|
161
|
+
|
|
162
|
+
Options:
|
|
163
|
+
--replace Capture new canonical evidence, then update an existing alias.
|
|
164
|
+
--help Show this help.
|
|
165
|
+
|
|
166
|
+
URL, viewport, targets, and output are read from the discovered project configuration.
|
|
167
|
+
`;
|
|
168
|
+
const CHECK_HELP = `Usage:
|
|
169
|
+
my-frontend-observer check [<baseline>] [--json]
|
|
170
|
+
|
|
171
|
+
Uses defaultBaseline when the alias is omitted. Captures the reserved current
|
|
172
|
+
candidate, persists a canonical comparison, and evaluates configured contract
|
|
173
|
+
and approved-reference acceptance. Comparison alone returns REVIEW_REQUIRED.
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
--json Print exactly one bounded CheckWorkflowResult JSON document.
|
|
177
|
+
--help Show this help.
|
|
178
|
+
|
|
179
|
+
Exit codes: PASS 0; FAIL 1; REVIEW_REQUIRED 2; BLOCKED 3.
|
|
180
|
+
`;
|
|
181
|
+
const OBSERVE_HELP = `Usage:
|
|
182
|
+
my-frontend-observer observe --url <loopback-url> [options]
|
|
183
|
+
|
|
184
|
+
Required:
|
|
185
|
+
--url <url> Loopback target URL (http/https, localhost/127.x.x.x/::1 only).
|
|
186
|
+
|
|
187
|
+
Options:
|
|
188
|
+
--viewport <WIDTHxHEIGHT> Viewport size, e.g. 1280x720.
|
|
189
|
+
--target <id=selector> An explicit CSS-shorthand observation target.
|
|
190
|
+
Repeatable. Parsed on the first "=" only, so
|
|
191
|
+
selectors containing "=" (e.g.
|
|
192
|
+
button[data-state="active"]) are preserved
|
|
193
|
+
intact. Cannot be combined with --targets-file.
|
|
194
|
+
--targets-file <json-file> Loads structured semantic observation targets
|
|
195
|
+
from a local JSON file: { "targets": [ { "name":
|
|
196
|
+
"...", "locators": [ { "kind": "role"|"id"|
|
|
197
|
+
"data-attribute"|"semantic-element"|"css"|"text",
|
|
198
|
+
... } ] } ] }. Locator order within a target is
|
|
199
|
+
the fallback order. Relative paths resolve from
|
|
200
|
+
the current working directory; the file path
|
|
201
|
+
itself is never persisted into the artifact or
|
|
202
|
+
included in the observation's request identity.
|
|
203
|
+
Cannot be combined with --target.
|
|
204
|
+
--scroll-scenario-file <json-file>
|
|
205
|
+
Loads one bounded runtime scroll scenario from a
|
|
206
|
+
local JSON file: { "action": { "kind":
|
|
207
|
+
"window-scroll-by"|"target-scroll-by", ...,
|
|
208
|
+
"deltaX": <int>, "deltaY": <int> } }.
|
|
209
|
+
"target-scroll-by" additionally requires a
|
|
210
|
+
"target" naming a configured stable target.
|
|
211
|
+
Exactly zero or one scenario per observation.
|
|
212
|
+
Relative paths resolve from the current working
|
|
213
|
+
directory; the file path itself is never
|
|
214
|
+
persisted into the artifact or included in the
|
|
215
|
+
observation's request identity. May be combined
|
|
216
|
+
with either --target or --targets-file.
|
|
217
|
+
--state-file <json-file> Loads explicit, caller-declared frontend state
|
|
218
|
+
identity from a local JSON file: { "theme":
|
|
219
|
+
"...", "applicationState": "...",
|
|
220
|
+
"authenticatedState": "authenticated"|
|
|
221
|
+
"unauthenticated" } (each field independently
|
|
222
|
+
optional; at least one required). Never inferred
|
|
223
|
+
by the observer from screenshot pixels, CSS, DOM,
|
|
224
|
+
or URLs - this is caller-declared metadata only,
|
|
225
|
+
used solely for later comparability/compatibility
|
|
226
|
+
evaluation. Relative paths resolve from the
|
|
227
|
+
current working directory; the file path itself
|
|
228
|
+
is never persisted into the artifact or included
|
|
229
|
+
in the observation's request identity.
|
|
230
|
+
--output <directory> Portable, relative output location for the
|
|
231
|
+
observation artifact.
|
|
232
|
+
--timeout <ms> Overall request timeout in milliseconds.
|
|
233
|
+
--help Show this help.
|
|
234
|
+
|
|
235
|
+
On success, prints a concise result and exits 0. On invalid syntax, invalid
|
|
236
|
+
request, unsafe/failed navigation, or a failed artifact write, prints
|
|
237
|
+
structured diagnostics to stderr and exits nonzero. No progress output is
|
|
238
|
+
printed during a normal capture.
|
|
239
|
+
`;
|
|
240
|
+
const COMPARE_HELP = `Usage:
|
|
241
|
+
my-frontend-observer compare --before <observation-artifact-root> --after <observation-artifact-root> --output <directory> [options]
|
|
242
|
+
|
|
243
|
+
Required:
|
|
244
|
+
--before <path> Root directory of the "before" persisted
|
|
245
|
+
observation artifact (the directory containing
|
|
246
|
+
its manifest.json).
|
|
247
|
+
--after <path> Root directory of the "after" persisted
|
|
248
|
+
observation artifact.
|
|
249
|
+
--output <directory> Portable, relative output location for the
|
|
250
|
+
comparison artifact.
|
|
251
|
+
|
|
252
|
+
Options:
|
|
253
|
+
--config-file <json-file> Loads a comparison configuration from a local
|
|
254
|
+
JSON file: { "geometryTolerancePx": <0-10>,
|
|
255
|
+
"expectedDependencies": [ { "cause": { "target":
|
|
256
|
+
"...", "property": "x"|"y"|"width"|"height",
|
|
257
|
+
"direction": "increase"|"decrease"|"change"|
|
|
258
|
+
"unchanged" }, "effect": { ... same shape ... },
|
|
259
|
+
"source": "explicit-config" } ] }. Relative
|
|
260
|
+
paths resolve from the current working
|
|
261
|
+
directory; the file path itself is never
|
|
262
|
+
persisted into the artifact or included in the
|
|
263
|
+
comparison request identity. Without
|
|
264
|
+
--config-file, geometryTolerancePx defaults to
|
|
265
|
+
0.5 with no declared dependencies.
|
|
266
|
+
--help Show this help.
|
|
267
|
+
|
|
268
|
+
Comparison reads two already-persisted observation artifacts and derives
|
|
269
|
+
evidence purely from their existing content - it never launches a browser
|
|
270
|
+
and never re-observes either target. On success, prints a concise result
|
|
271
|
+
and exits 0, including when the two observations are found to be
|
|
272
|
+
"incomparable" (that is itself a successful comparison outcome, not a
|
|
273
|
+
failure). On invalid syntax, an unreadable or structurally invalid source
|
|
274
|
+
artifact, invalid configuration, or a failed artifact write, prints
|
|
275
|
+
structured diagnostics to stderr and exits nonzero. No progress output is
|
|
276
|
+
printed during a normal comparison.
|
|
277
|
+
`;
|
|
278
|
+
const APPROVE_BASELINE_HELP = `Usage:
|
|
279
|
+
my-frontend-observer approve-baseline --observation <observation-artifact-root> --contract-file <json-file> --output <directory>
|
|
280
|
+
|
|
281
|
+
Required:
|
|
282
|
+
--observation <path> Root directory of the persisted observation
|
|
283
|
+
artifact (the directory containing its
|
|
284
|
+
manifest.json) that this baseline claims to
|
|
285
|
+
approve.
|
|
286
|
+
--contract-file <json-file> Local JSON file containing one already-authored
|
|
287
|
+
persistent baseline contract (the raw contract
|
|
288
|
+
value, no wrapper field). Relative paths resolve
|
|
289
|
+
from the current working directory; the file
|
|
290
|
+
path itself is never persisted or included in
|
|
291
|
+
any identity.
|
|
292
|
+
--output <directory> Portable, relative output location for the
|
|
293
|
+
baseline artifact.
|
|
294
|
+
|
|
295
|
+
Options:
|
|
296
|
+
--help Show this help.
|
|
297
|
+
|
|
298
|
+
This command is the only explicit baseline-approval act in the observer -
|
|
299
|
+
approval is never inferred from a successful comparison or evaluation. The
|
|
300
|
+
supplied contract's source-observation reference must match the supplied
|
|
301
|
+
observation artifact's stable identity; a mismatched or unrelated observation
|
|
302
|
+
is rejected. Any \`supersedesBaselineId\` already authored in the contract is
|
|
303
|
+
preserved exactly - this command never discovers, infers, or deletes a prior
|
|
304
|
+
baseline. Remains local and non-mutating: it never launches a browser and
|
|
305
|
+
never modifies the source observation or any existing baseline artifact. On
|
|
306
|
+
success, prints a concise result and exits 0. On invalid syntax, an
|
|
307
|
+
unreadable/malformed contract file, a non-baseline contract, a
|
|
308
|
+
structurally invalid contract, a source-observation mismatch, an existing
|
|
309
|
+
artifact collision, or a persistence failure, prints structured diagnostics
|
|
310
|
+
to stderr and exits nonzero.
|
|
311
|
+
`;
|
|
312
|
+
const SAVE_CHANGE_CONTRACT_HELP = `Usage:
|
|
313
|
+
my-frontend-observer save-change-contract --contract-file <json-file> --output <directory>
|
|
314
|
+
|
|
315
|
+
Required:
|
|
316
|
+
--contract-file <json-file> Local JSON file containing one already-authored
|
|
317
|
+
per-change contract (the raw contract value, no
|
|
318
|
+
wrapper field). Relative paths resolve from the
|
|
319
|
+
current working directory; the file path itself
|
|
320
|
+
is never persisted or included in any identity.
|
|
321
|
+
--output <directory> Portable, relative output location for the
|
|
322
|
+
change-contract artifact.
|
|
323
|
+
|
|
324
|
+
Options:
|
|
325
|
+
--help Show this help.
|
|
326
|
+
|
|
327
|
+
This command validates and persists a per-change contract only - it does not
|
|
328
|
+
approve anything. Any \`supersedesBaselineClauseIds\` already authored on a
|
|
329
|
+
clause is preserved exactly; resolving those references against a particular
|
|
330
|
+
baseline remains \`evaluate-contract\`'s responsibility, not this command's.
|
|
331
|
+
Remains local and non-mutating. On success, prints a concise result and
|
|
332
|
+
exits 0. On invalid syntax, an unreadable/malformed contract file, a
|
|
333
|
+
non-change contract (e.g. a persistent baseline contract), a structurally
|
|
334
|
+
invalid contract (including an authored \`unexpected\` category, which is
|
|
335
|
+
never a valid authored scope), an existing artifact collision, or a
|
|
336
|
+
persistence failure, prints structured diagnostics to stderr and exits
|
|
337
|
+
nonzero.
|
|
338
|
+
`;
|
|
339
|
+
const EVALUATE_CONTRACT_HELP = `Usage:
|
|
340
|
+
my-frontend-observer evaluate-contract --before <observation-artifact-root> --after <observation-artifact-root> --comparison <comparison-artifact-root> --baseline <baseline-contract-artifact-root> --change <per-change-contract-artifact-root> --output <directory> [--enforce]
|
|
341
|
+
|
|
342
|
+
Required:
|
|
343
|
+
--before <path> Root directory of the "before" persisted observation
|
|
344
|
+
artifact.
|
|
345
|
+
--after <path> Root directory of the "after" persisted observation
|
|
346
|
+
artifact.
|
|
347
|
+
--comparison <path> Root directory of the already-persisted comparison
|
|
348
|
+
artifact for that before/after pair.
|
|
349
|
+
--baseline <path> Root directory of the already-approved persistent
|
|
350
|
+
baseline contract artifact.
|
|
351
|
+
--change <path> Root directory of the already-persisted per-change
|
|
352
|
+
contract artifact.
|
|
353
|
+
--output <directory> Portable, relative output location for the
|
|
354
|
+
evaluation artifact.
|
|
355
|
+
|
|
356
|
+
Options:
|
|
357
|
+
--enforce Make a FAIL verdict produce a nonzero process exit status. A
|
|
358
|
+
FAIL evaluation is always persisted and printed identically
|
|
359
|
+
with or without this flag - it changes only the process exit
|
|
360
|
+
code, never evaluation identity, contents, or persistence.
|
|
361
|
+
--help Show this help.
|
|
362
|
+
|
|
363
|
+
This command never launches a browser, never re-resolves targets, and never
|
|
364
|
+
recomputes comparison or relationship evidence - it reads the already-
|
|
365
|
+
persisted before/after observations and comparison exactly as given and
|
|
366
|
+
evaluates the supplied baseline/change contracts against them exactly once.
|
|
367
|
+
A FAIL verdict (a found regression or unsatisfied contract clause) is a
|
|
368
|
+
successful, persisted evaluation outcome, not an execution error; without
|
|
369
|
+
--enforce it exits 0 like PASS. On success (evaluation constructed and
|
|
370
|
+
persisted, verdict PASS, or verdict FAIL without --enforce), prints a
|
|
371
|
+
concise result and exits 0. With --enforce and verdict FAIL, prints the same
|
|
372
|
+
result and exits nonzero. On invalid syntax, an unreadable/malformed/
|
|
373
|
+
incoherent source artifact, or a persistence failure (evaluation could not
|
|
374
|
+
even be constructed), prints structured diagnostics to stderr, persists
|
|
375
|
+
nothing, and exits nonzero.
|
|
376
|
+
`;
|
|
377
|
+
const IMPORT_REFERENCE_HELP = `Usage:
|
|
378
|
+
my-frontend-observer import-reference <image-file> --output <directory> [options]
|
|
379
|
+
|
|
380
|
+
Required:
|
|
381
|
+
<image-file> Local path to a PNG, JPEG, or WebP external design-
|
|
382
|
+
reference image.
|
|
383
|
+
--output <directory> Portable, relative output location for the
|
|
384
|
+
external-reference artifact.
|
|
385
|
+
|
|
386
|
+
Options:
|
|
387
|
+
--label <text> Optional human-readable label, stored as pure
|
|
388
|
+
provenance - never part of the reference's logical
|
|
389
|
+
identity.
|
|
390
|
+
--supersedes <path> Root directory of a prior external-reference
|
|
391
|
+
artifact (imported or approved) that this import
|
|
392
|
+
explicitly supersedes. The prior artifact is never
|
|
393
|
+
modified.
|
|
394
|
+
--regions-file <json-file> Local JSON file of the form { "regions": [...] }
|
|
395
|
+
declaring explicit, meaningful reference-image
|
|
396
|
+
regions (id + a {x, y, width, height} rectangle in
|
|
397
|
+
reference-image pixels, origin at the image's
|
|
398
|
+
top-left corner). Optional - a reference imported
|
|
399
|
+
without this flag behaves exactly as in v0.7 Prompt
|
|
400
|
+
1. Region content participates in the reference's
|
|
401
|
+
logical identity; the file path itself never does.
|
|
402
|
+
--requirements-file <json-file> Local JSON file of the form
|
|
403
|
+
{ "requirements": [...] } declaring explicit,
|
|
404
|
+
user-selected design requirements over the regions
|
|
405
|
+
above - what actually matters for later candidate
|
|
406
|
+
evaluation, never inferred merely because a region
|
|
407
|
+
property/relationship exists. Each requirement has
|
|
408
|
+
a "category" (requested | expected-dependent |
|
|
409
|
+
protected | preserved - "unexpected" is never
|
|
410
|
+
authorable), a "subject" (a region property, a
|
|
411
|
+
region-to-region relationship, or a derived
|
|
412
|
+
two-region measurement), and - for property/
|
|
413
|
+
measurement subjects - a "tolerance" (exact |
|
|
414
|
+
absolute-reference-px | percent; relationship
|
|
415
|
+
subjects must omit tolerance). Requires --regions-
|
|
416
|
+
file (or an already-present region set) supplying
|
|
417
|
+
every region a requirement refers to. Optional -
|
|
418
|
+
a reference imported without this flag behaves
|
|
419
|
+
exactly as in v0.7 Prompt 1/2. Requirement content
|
|
420
|
+
participates in the reference's logical identity.
|
|
421
|
+
--applicability-file <json-file> Local JSON file declaring the runtime
|
|
422
|
+
frontend state this reference is intended to
|
|
423
|
+
represent: { "viewport": { "width", "height" },
|
|
424
|
+
"theme": "...", "applicationState": "...",
|
|
425
|
+
"authenticatedState": "authenticated"|
|
|
426
|
+
"unauthenticated" } (each field independently
|
|
427
|
+
optional; at least one required). "viewport" here
|
|
428
|
+
is the CSS-pixel runtime viewport the design
|
|
429
|
+
represents - distinct from the reference image's
|
|
430
|
+
own pixel dimensions, which are never assumed
|
|
431
|
+
equal. Never inferred from the image - caller-
|
|
432
|
+
declared metadata only, used for later reference/
|
|
433
|
+
candidate compatibility evaluation (see
|
|
434
|
+
docs/CONTRACTS.md "v0.7 Prompt 4"). Optional - a
|
|
435
|
+
reference imported without this flag behaves
|
|
436
|
+
exactly as in v0.7 Prompt 1/2/3. Applicability
|
|
437
|
+
content participates in the reference's logical
|
|
438
|
+
identity.
|
|
439
|
+
--help Show this help.
|
|
440
|
+
|
|
441
|
+
Detects the image format from its header bytes only (never from the file
|
|
442
|
+
extension), reads its pixel dimensions from the same bounded header bytes
|
|
443
|
+
(never decoding pixel data), and persists a new external-reference artifact
|
|
444
|
+
in the "imported" lifecycle state - importing never approves it. On success,
|
|
445
|
+
prints a concise result (including the accepted region/requirement counts,
|
|
446
|
+
the resulting reference-side requirement adequacy: adequate, partial, or
|
|
447
|
+
inadequate, and whether applicability was declared) and exits 0. On an
|
|
448
|
+
unreadable file, an unsupported or undetectable format, invalid/out-of-bound
|
|
449
|
+
dimensions, an over-limit file size, an unresolvable --supersedes target, an
|
|
450
|
+
invalid region (missing/duplicate/malformed id, non-finite/negative/zero
|
|
451
|
+
geometry, a region extending outside the image, or more than the bounded
|
|
452
|
+
maximum region count), an invalid requirement (unsupported category/
|
|
453
|
+
property/measurement/relationship, a tolerance that is missing/inapplicable/
|
|
454
|
+
out of bounds, a reference to an unknown region id, a duplicate requirement
|
|
455
|
+
subject, or more than the bounded maximum requirement count), or invalid
|
|
456
|
+
applicability (an out-of-bound viewport, an invalid state label, an
|
|
457
|
+
unsupported authenticatedState value, or an empty applicability object),
|
|
458
|
+
prints structured diagnostics to stderr and exits nonzero.
|
|
459
|
+
`;
|
|
460
|
+
const APPROVE_REFERENCE_HELP = `Usage:
|
|
461
|
+
my-frontend-observer approve-reference --reference <external-reference-artifact-root> --output <directory> [options]
|
|
462
|
+
|
|
463
|
+
Required:
|
|
464
|
+
--reference <path> Root directory of the already-imported
|
|
465
|
+
external-reference artifact (the directory
|
|
466
|
+
containing its manifest.json) to approve.
|
|
467
|
+
--output <directory> Portable, relative output location for the newly
|
|
468
|
+
persisted approved artifact.
|
|
469
|
+
|
|
470
|
+
Options:
|
|
471
|
+
--supersedes <path> Root directory of a prior external-reference
|
|
472
|
+
artifact (imported or approved) that this approval
|
|
473
|
+
explicitly supersedes. The prior artifact is never
|
|
474
|
+
modified.
|
|
475
|
+
--help Show this help.
|
|
476
|
+
|
|
477
|
+
This is the only explicit reference-approval act in the observer - approval
|
|
478
|
+
is never inferred from a successful import or from any later fidelity
|
|
479
|
+
evaluation. Approving persists a brand-new artifact instance (a fresh
|
|
480
|
+
referenceId sharing the imported artifact's referenceRequestId) that carries
|
|
481
|
+
a reference back to the imported artifact's image rather than a second copy
|
|
482
|
+
of its bytes; the imported artifact's own manifest is never modified. Any
|
|
483
|
+
regions, requirements, and applicability already declared on the imported
|
|
484
|
+
artifact are carried forward unchanged (not re-validated against new input,
|
|
485
|
+
not re-derived) - approval never adds, removes, or edits regions,
|
|
486
|
+
requirements, or applicability. Only a reference currently in the "imported"
|
|
487
|
+
lifecycle state can be approved. On success, prints a concise result
|
|
488
|
+
(including the carried-forward region/requirement counts, reference-side
|
|
489
|
+
requirement adequacy, and whether applicability was declared) and exits 0.
|
|
490
|
+
On an unreadable/malformed --reference target, a target that is not in the
|
|
491
|
+
"imported" state, an unresolvable --supersedes target, or a persistence
|
|
492
|
+
failure, prints structured diagnostics to stderr and exits nonzero.
|
|
493
|
+
`;
|
|
494
|
+
const EVALUATE_REFERENCE_FIDELITY_HELP = `Usage:
|
|
495
|
+
my-frontend-observer evaluate-reference-fidelity --reference <external-reference-artifact-root> --candidate <observation-artifact-root> [options]
|
|
496
|
+
|
|
497
|
+
Required:
|
|
498
|
+
--reference <path> Root directory of an already-imported or already-
|
|
499
|
+
approved external-reference artifact (the directory
|
|
500
|
+
containing its manifest.json).
|
|
501
|
+
--candidate <path> Root directory of the already-persisted candidate
|
|
502
|
+
observation artifact to evaluate against it.
|
|
503
|
+
|
|
504
|
+
Options:
|
|
505
|
+
--bindings-file <json-file> Local JSON file of the form
|
|
506
|
+
{ "bindings": [ { "referenceRegion": "...",
|
|
507
|
+
"runtimeTarget": "..." } ] } declaring which stable
|
|
508
|
+
observer runtime target (a configured target name -
|
|
509
|
+
see "observe" --target/--targets-file) explicitly
|
|
510
|
+
corresponds to each reference region a selected
|
|
511
|
+
requirement depends on. Never inferred from
|
|
512
|
+
geometry, matching names, or source code - a
|
|
513
|
+
binding exists only because this file declares it.
|
|
514
|
+
Optional - omitting it (or supplying an empty
|
|
515
|
+
"bindings" array) evaluates with no bindings at
|
|
516
|
+
all, so every requirement whose subject depends on
|
|
517
|
+
a reference region becomes "unavailable".
|
|
518
|
+
--enforce Make a FAIL fidelity result produce a nonzero process exit
|
|
519
|
+
status. A FAIL result is always printed identically with or
|
|
520
|
+
without this flag - it changes only the process exit code,
|
|
521
|
+
never the evaluation's content. Has no effect on a
|
|
522
|
+
"not-evaluated" result (a reference-adequacy or compatibility
|
|
523
|
+
blocker is never treated as a design mismatch).
|
|
524
|
+
--help Show this help.
|
|
525
|
+
|
|
526
|
+
This command never launches a browser, never re-resolves targets, and
|
|
527
|
+
never recomputes reference regions/requirements/adequacy, compatibility, or
|
|
528
|
+
bindings - it reads the already-persisted reference and candidate exactly
|
|
529
|
+
as given, evaluates the supplied binding declarations, and evaluates every
|
|
530
|
+
one of the reference's selected requirements exactly once. It persists
|
|
531
|
+
nothing: the result exists only for this invocation. On success, prints a
|
|
532
|
+
concise result (reference-side adequacy, compatibility state, the overall
|
|
533
|
+
fidelity state - "not-evaluated"/"pass"/"fail" - and a pass/fail/unavailable
|
|
534
|
+
requirement breakdown) and exits 0, unless --enforce is given and the
|
|
535
|
+
fidelity state is "fail", in which case it exits nonzero. A "not-evaluated"
|
|
536
|
+
result (reference adequacy inadequate, or reference/candidate
|
|
537
|
+
incompatible) is a successful, structured evaluation outcome, never an
|
|
538
|
+
execution error - it always exits 0. On invalid syntax, an unreadable/
|
|
539
|
+
malformed --reference or --candidate target, a malformed --bindings-file,
|
|
540
|
+
or an invalid/out-of-bound binding declaration, prints structured
|
|
541
|
+
diagnostics to stderr and exits nonzero.
|
|
542
|
+
`;
|
|
543
|
+
function parseViewport(raw) {
|
|
544
|
+
const match = /^(\d+)x(\d+)$/.exec(raw);
|
|
545
|
+
if (!match)
|
|
546
|
+
return undefined;
|
|
547
|
+
const width = Number(match[1]);
|
|
548
|
+
const height = Number(match[2]);
|
|
549
|
+
return { width, height };
|
|
550
|
+
}
|
|
551
|
+
function parseTarget(raw) {
|
|
552
|
+
const eq = raw.indexOf('=');
|
|
553
|
+
if (eq <= 0 || eq === raw.length - 1)
|
|
554
|
+
return undefined;
|
|
555
|
+
return { name: raw.slice(0, eq), selector: raw.slice(eq + 1) };
|
|
556
|
+
}
|
|
557
|
+
/** CLI-syntax-only parsing: shape/format errors only. Domain bounds and policy are Batch 1's job, not this function's. */
|
|
558
|
+
function parseObserveArgs(argv) {
|
|
559
|
+
const errors = [];
|
|
560
|
+
let targetUrl;
|
|
561
|
+
let viewport;
|
|
562
|
+
const targets = [];
|
|
563
|
+
let outputLocation;
|
|
564
|
+
let timeoutMs;
|
|
565
|
+
let targetsFilePath;
|
|
566
|
+
let targetsFileFlagCount = 0;
|
|
567
|
+
let scrollScenarioFilePath;
|
|
568
|
+
let scrollScenarioFileFlagCount = 0;
|
|
569
|
+
let stateFilePath;
|
|
570
|
+
let stateFileFlagCount = 0;
|
|
571
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
572
|
+
const arg = argv[i];
|
|
573
|
+
switch (arg) {
|
|
574
|
+
case '--url':
|
|
575
|
+
targetUrl = argv[(i += 1)];
|
|
576
|
+
break;
|
|
577
|
+
case '--viewport': {
|
|
578
|
+
const value = argv[(i += 1)];
|
|
579
|
+
const parsed = value === undefined ? undefined : parseViewport(value);
|
|
580
|
+
if (parsed === undefined) {
|
|
581
|
+
errors.push(`--viewport must be WIDTHxHEIGHT (e.g. 1280x720); got ${JSON.stringify(value)}`);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
viewport = parsed;
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case '--target': {
|
|
589
|
+
const value = argv[(i += 1)];
|
|
590
|
+
const parsed = value === undefined ? undefined : parseTarget(value);
|
|
591
|
+
if (parsed === undefined) {
|
|
592
|
+
errors.push(`--target must be id=css-selector; got ${JSON.stringify(value)}`);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
targets.push(parsed);
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case '--targets-file': {
|
|
600
|
+
const value = argv[(i += 1)];
|
|
601
|
+
targetsFileFlagCount += 1;
|
|
602
|
+
if (value === undefined) {
|
|
603
|
+
errors.push('--targets-file requires a file path argument');
|
|
604
|
+
}
|
|
605
|
+
else if (targetsFileFlagCount > 1) {
|
|
606
|
+
errors.push('--targets-file may only be specified once');
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
targetsFilePath = value;
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case '--scroll-scenario-file': {
|
|
614
|
+
const value = argv[(i += 1)];
|
|
615
|
+
scrollScenarioFileFlagCount += 1;
|
|
616
|
+
if (value === undefined) {
|
|
617
|
+
errors.push('--scroll-scenario-file requires a file path argument');
|
|
618
|
+
}
|
|
619
|
+
else if (scrollScenarioFileFlagCount > 1) {
|
|
620
|
+
errors.push('--scroll-scenario-file may only be specified once');
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
scrollScenarioFilePath = value;
|
|
624
|
+
}
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case '--state-file': {
|
|
628
|
+
const value = argv[(i += 1)];
|
|
629
|
+
stateFileFlagCount += 1;
|
|
630
|
+
if (value === undefined) {
|
|
631
|
+
errors.push('--state-file requires a file path argument');
|
|
632
|
+
}
|
|
633
|
+
else if (stateFileFlagCount > 1) {
|
|
634
|
+
errors.push('--state-file may only be specified once');
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
stateFilePath = value;
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case '--output':
|
|
642
|
+
outputLocation = argv[(i += 1)];
|
|
643
|
+
break;
|
|
644
|
+
case '--timeout': {
|
|
645
|
+
const value = argv[(i += 1)];
|
|
646
|
+
const parsedNumber = value === undefined ? NaN : Number(value);
|
|
647
|
+
if (!Number.isFinite(parsedNumber)) {
|
|
648
|
+
errors.push(`--timeout must be a number of milliseconds; got ${JSON.stringify(value)}`);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
timeoutMs = parsedNumber;
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
default:
|
|
656
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (targetUrl === undefined)
|
|
660
|
+
errors.push('--url is required');
|
|
661
|
+
if (targets.length > 0 && targetsFilePath !== undefined) {
|
|
662
|
+
errors.push('--target and --targets-file cannot be combined; use one or the other');
|
|
663
|
+
}
|
|
664
|
+
if (errors.length > 0)
|
|
665
|
+
return { ok: false, errors };
|
|
666
|
+
const raw = {
|
|
667
|
+
targetUrl,
|
|
668
|
+
...(viewport === undefined ? {} : { viewport }),
|
|
669
|
+
...(targets.length > 0 ? { targets } : {}),
|
|
670
|
+
...(outputLocation === undefined ? {} : { outputLocation }),
|
|
671
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
672
|
+
};
|
|
673
|
+
return {
|
|
674
|
+
ok: true,
|
|
675
|
+
raw,
|
|
676
|
+
...(targetsFilePath === undefined ? {} : { targetsFilePath }),
|
|
677
|
+
...(scrollScenarioFilePath === undefined ? {} : { scrollScenarioFilePath }),
|
|
678
|
+
...(stateFilePath === undefined ? {} : { stateFilePath }),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const TARGETS_FILE_ALLOWED_ROOT_FIELDS = new Set(['targets']);
|
|
682
|
+
const REGIONS_FILE_ALLOWED_ROOT_FIELDS = new Set(['regions']);
|
|
683
|
+
/**
|
|
684
|
+
* CLI/input-boundary-only responsibility, mirroring loadTargetsFile exactly:
|
|
685
|
+
* read one local JSON file, validate only the root wrapper (object root,
|
|
686
|
+
* exactly the "regions" field, nothing else), and hand the still-unvalidated
|
|
687
|
+
* `regions` value to the existing domain validators
|
|
688
|
+
* (isValidReferenceRegions, called inside importExternalReference) - region
|
|
689
|
+
* geometry/ID rules stay owned there, never duplicated here. The file path
|
|
690
|
+
* itself is never returned beyond this function, so it can never reach the
|
|
691
|
+
* persisted artifact or its identity.
|
|
692
|
+
*/
|
|
693
|
+
function loadRegionsFile(filePath) {
|
|
694
|
+
let rawText;
|
|
695
|
+
try {
|
|
696
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
700
|
+
return { ok: false, error: `--regions-file could not be read: ${message}` };
|
|
701
|
+
}
|
|
702
|
+
let parsed;
|
|
703
|
+
try {
|
|
704
|
+
parsed = JSON.parse(rawText);
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
return { ok: false, error: `--regions-file is not valid JSON: ${message}` };
|
|
709
|
+
}
|
|
710
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
711
|
+
return { ok: false, error: '--regions-file root must be a JSON object' };
|
|
712
|
+
}
|
|
713
|
+
const record = parsed;
|
|
714
|
+
const unknownFields = Object.keys(record).filter((key) => !REGIONS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
715
|
+
if (unknownFields.length > 0) {
|
|
716
|
+
return { ok: false, error: `--regions-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
717
|
+
}
|
|
718
|
+
if (!('regions' in record)) {
|
|
719
|
+
return { ok: false, error: '--regions-file must have a "regions" property' };
|
|
720
|
+
}
|
|
721
|
+
return { ok: true, regions: record.regions };
|
|
722
|
+
}
|
|
723
|
+
const REQUIREMENTS_FILE_ALLOWED_ROOT_FIELDS = new Set(['requirements']);
|
|
724
|
+
/**
|
|
725
|
+
* CLI/input-boundary-only responsibility, mirroring loadRegionsFile exactly:
|
|
726
|
+
* read one local JSON file, validate only the root wrapper (object root,
|
|
727
|
+
* exactly the "requirements" field, nothing else), and hand the
|
|
728
|
+
* still-unvalidated `requirements` value to the existing domain validators
|
|
729
|
+
* (isValidRawReferenceRequirement/isValidReferenceRequirements, called
|
|
730
|
+
* inside importExternalReference) - requirement category/subject/tolerance
|
|
731
|
+
* rules stay owned there, never duplicated here. The file path itself is
|
|
732
|
+
* never returned beyond this function, so it can never reach the persisted
|
|
733
|
+
* artifact or its identity.
|
|
734
|
+
*/
|
|
735
|
+
function loadRequirementsFile(filePath) {
|
|
736
|
+
let rawText;
|
|
737
|
+
try {
|
|
738
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
742
|
+
return { ok: false, error: `--requirements-file could not be read: ${message}` };
|
|
743
|
+
}
|
|
744
|
+
let parsed;
|
|
745
|
+
try {
|
|
746
|
+
parsed = JSON.parse(rawText);
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
750
|
+
return { ok: false, error: `--requirements-file is not valid JSON: ${message}` };
|
|
751
|
+
}
|
|
752
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
753
|
+
return { ok: false, error: '--requirements-file root must be a JSON object' };
|
|
754
|
+
}
|
|
755
|
+
const record = parsed;
|
|
756
|
+
const unknownFields = Object.keys(record).filter((key) => !REQUIREMENTS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
757
|
+
if (unknownFields.length > 0) {
|
|
758
|
+
return { ok: false, error: `--requirements-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
759
|
+
}
|
|
760
|
+
if (!('requirements' in record)) {
|
|
761
|
+
return { ok: false, error: '--requirements-file must have a "requirements" property' };
|
|
762
|
+
}
|
|
763
|
+
return { ok: true, requirements: record.requirements };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* CLI/input-boundary-only responsibility, mirroring `loadStateFile`/
|
|
767
|
+
* `loadScrollScenarioFile`: read one local JSON file and validate only the
|
|
768
|
+
* root shape (plain, non-array object) - the file supplies
|
|
769
|
+
* `ExternalReferenceApplicability` directly (no wrapper field), so there is
|
|
770
|
+
* no root-field allowlist to enforce here. Every applicability rule
|
|
771
|
+
* (viewport bounds, state-label pattern, authenticatedState enum) stays
|
|
772
|
+
* owned by `isValidExternalReferenceApplicability`, not duplicated here. The
|
|
773
|
+
* file path itself is never returned beyond this function, so it can never
|
|
774
|
+
* reach the persisted artifact or its identity.
|
|
775
|
+
*/
|
|
776
|
+
function loadApplicabilityFile(filePath) {
|
|
777
|
+
let rawText;
|
|
778
|
+
try {
|
|
779
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
783
|
+
return { ok: false, error: `--applicability-file could not be read: ${message}` };
|
|
784
|
+
}
|
|
785
|
+
let parsed;
|
|
786
|
+
try {
|
|
787
|
+
parsed = JSON.parse(rawText);
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
791
|
+
return { ok: false, error: `--applicability-file is not valid JSON: ${message}` };
|
|
792
|
+
}
|
|
793
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
794
|
+
return { ok: false, error: '--applicability-file root must be a JSON object' };
|
|
795
|
+
}
|
|
796
|
+
return { ok: true, applicability: parsed };
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* v0.8 Batch 7 CLI/input-boundary-only responsibility, mirroring
|
|
800
|
+
* `loadBindingsFile`'s exact shape: read one local JSON file (size-bounded
|
|
801
|
+
* via `MAX_CONTEXT_FILE_BYTES`, checked via `statSync` before ever reading
|
|
802
|
+
* the file's bytes), parse it, and hand the parsed value to
|
|
803
|
+
* `classifyContextFileContent` - every artifactKind/schemaVersion/structural
|
|
804
|
+
* rule stays owned there (which itself defers all current-schema structural
|
|
805
|
+
* validation to the existing canonical `isValidBoundedAgentContextArtifact`,
|
|
806
|
+
* never a second validator). Unlike `--bindings-file`, the context file's
|
|
807
|
+
* root IS the artifact value directly (task §12) - no wrapper object. The
|
|
808
|
+
* file path itself is never returned beyond this function.
|
|
809
|
+
*/
|
|
810
|
+
function loadContextFile(filePath) {
|
|
811
|
+
let size;
|
|
812
|
+
try {
|
|
813
|
+
size = statSync(filePath).size;
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
817
|
+
return { ok: false, error: `--context-file could not be read: ${message}` };
|
|
818
|
+
}
|
|
819
|
+
if (size > MAX_CONTEXT_FILE_BYTES) {
|
|
820
|
+
return { ok: false, error: `--context-file exceeds the bounded size limit (${MAX_CONTEXT_FILE_BYTES} bytes)` };
|
|
821
|
+
}
|
|
822
|
+
let rawText;
|
|
823
|
+
try {
|
|
824
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
828
|
+
return { ok: false, error: `--context-file could not be read: ${message}` };
|
|
829
|
+
}
|
|
830
|
+
let parsed;
|
|
831
|
+
try {
|
|
832
|
+
parsed = JSON.parse(rawText);
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
836
|
+
return { ok: false, error: `--context-file is not valid JSON: ${message}` };
|
|
837
|
+
}
|
|
838
|
+
const classified = classifyContextFileContent(parsed);
|
|
839
|
+
if (!classified.ok)
|
|
840
|
+
return { ok: false, error: classified.error };
|
|
841
|
+
return { ok: true, state: classified.state };
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* CLI/input-boundary-only responsibility: read one local JSON file, validate
|
|
845
|
+
* only the root wrapper this file format owns (object root, exactly the
|
|
846
|
+
* "targets" field, nothing else), and hand the still-unvalidated `targets`
|
|
847
|
+
* value to the existing `normalizeRequest()` - every target/locator-internal
|
|
848
|
+
* rule (bounds, locator kinds, per-kind fields) stays owned there, not
|
|
849
|
+
* duplicated here. The file path itself is never returned to the caller
|
|
850
|
+
* beyond this function, so it can never reach the persisted request/artifact.
|
|
851
|
+
*/
|
|
852
|
+
function loadTargetsFile(filePath) {
|
|
853
|
+
let rawText;
|
|
854
|
+
try {
|
|
855
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
859
|
+
return { ok: false, error: `--targets-file could not be read: ${message}` };
|
|
860
|
+
}
|
|
861
|
+
let parsed;
|
|
862
|
+
try {
|
|
863
|
+
parsed = JSON.parse(rawText);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
867
|
+
return { ok: false, error: `--targets-file is not valid JSON: ${message}` };
|
|
868
|
+
}
|
|
869
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
870
|
+
return { ok: false, error: '--targets-file root must be a JSON object' };
|
|
871
|
+
}
|
|
872
|
+
const record = parsed;
|
|
873
|
+
const unknownFields = Object.keys(record).filter((key) => !TARGETS_FILE_ALLOWED_ROOT_FIELDS.has(key));
|
|
874
|
+
if (unknownFields.length > 0) {
|
|
875
|
+
return { ok: false, error: `--targets-file has unsupported top-level field(s): ${unknownFields.join(', ')}` };
|
|
876
|
+
}
|
|
877
|
+
if (!('targets' in record)) {
|
|
878
|
+
return { ok: false, error: '--targets-file must have a "targets" property' };
|
|
879
|
+
}
|
|
880
|
+
return { ok: true, targets: record.targets };
|
|
881
|
+
}
|
|
882
|
+
function parseInitArgs(argv) {
|
|
883
|
+
const errors = [];
|
|
884
|
+
let url;
|
|
885
|
+
let viewport = { width: 1280, height: 720 };
|
|
886
|
+
const targets = [];
|
|
887
|
+
let targetsFilePath;
|
|
888
|
+
let defaultBaseline;
|
|
889
|
+
let replace = false;
|
|
890
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
891
|
+
const arg = argv[i];
|
|
892
|
+
if (arg === '--url')
|
|
893
|
+
url = argv[(i += 1)];
|
|
894
|
+
else if (arg === '--viewport') {
|
|
895
|
+
const value = argv[(i += 1)];
|
|
896
|
+
const parsed = value === undefined ? undefined : parseViewport(value);
|
|
897
|
+
if (parsed === undefined)
|
|
898
|
+
errors.push(`--viewport must be WIDTHxHEIGHT (e.g. 1280x720); got ${JSON.stringify(value)}`);
|
|
899
|
+
else
|
|
900
|
+
viewport = parsed;
|
|
901
|
+
}
|
|
902
|
+
else if (arg === '--target') {
|
|
903
|
+
const value = argv[(i += 1)];
|
|
904
|
+
const parsed = value === undefined ? undefined : parseTarget(value);
|
|
905
|
+
if (parsed === undefined)
|
|
906
|
+
errors.push(`--target must be id=css-selector; got ${JSON.stringify(value)}`);
|
|
907
|
+
else
|
|
908
|
+
targets.push(parsed);
|
|
909
|
+
}
|
|
910
|
+
else if (arg === '--targets-file') {
|
|
911
|
+
const value = argv[(i += 1)];
|
|
912
|
+
if (value === undefined)
|
|
913
|
+
errors.push('--targets-file requires a file path argument');
|
|
914
|
+
else if (targetsFilePath !== undefined)
|
|
915
|
+
errors.push('--targets-file may only be specified once');
|
|
916
|
+
else
|
|
917
|
+
targetsFilePath = value;
|
|
918
|
+
}
|
|
919
|
+
else if (arg === '--default-baseline') {
|
|
920
|
+
const value = argv[(i += 1)];
|
|
921
|
+
if (value === undefined)
|
|
922
|
+
errors.push('--default-baseline requires an alias argument');
|
|
923
|
+
else
|
|
924
|
+
defaultBaseline = value;
|
|
925
|
+
}
|
|
926
|
+
else if (arg === '--replace')
|
|
927
|
+
replace = true;
|
|
928
|
+
else
|
|
929
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
930
|
+
}
|
|
931
|
+
if (url === undefined)
|
|
932
|
+
errors.push('--url is required');
|
|
933
|
+
if (targets.length > 0 && targetsFilePath !== undefined)
|
|
934
|
+
errors.push('--target and --targets-file cannot be combined; use one or the other');
|
|
935
|
+
if (errors.length > 0)
|
|
936
|
+
return { ok: false, errors };
|
|
937
|
+
return { ok: true, url: url, viewport, targets, ...(targetsFilePath === undefined ? {} : { targetsFilePath }), ...(defaultBaseline === undefined ? {} : { defaultBaseline }), replace };
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* CLI/input-boundary-only responsibility, mirroring `loadTargetsFile`: read
|
|
941
|
+
* one local JSON file and validate only the root shape this file format
|
|
942
|
+
* owns (plain, non-array object) - the file supplies the value of
|
|
943
|
+
* `RawObservationRequest.scrollScenario` directly (no wrapper field), so
|
|
944
|
+
* there is no root-field allowlist to enforce here the way
|
|
945
|
+
* `loadTargetsFile` enforces `{ "targets": [...] }`. Every scenario/action
|
|
946
|
+
* rule (supported kind, required action, delta types/bounds, the both-zero
|
|
947
|
+
* rule, stable target reference, unknown fields) stays owned by
|
|
948
|
+
* `normalizeRequest()`, not duplicated here. The file path itself is never
|
|
949
|
+
* returned to the caller beyond this function, so it can never reach the
|
|
950
|
+
* persisted request/artifact.
|
|
951
|
+
*/
|
|
952
|
+
function loadScrollScenarioFile(filePath) {
|
|
953
|
+
let rawText;
|
|
954
|
+
try {
|
|
955
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
959
|
+
return { ok: false, error: `--scroll-scenario-file could not be read: ${message}` };
|
|
960
|
+
}
|
|
961
|
+
let parsed;
|
|
962
|
+
try {
|
|
963
|
+
parsed = JSON.parse(rawText);
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
967
|
+
return { ok: false, error: `--scroll-scenario-file is not valid JSON: ${message}` };
|
|
968
|
+
}
|
|
969
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
970
|
+
return { ok: false, error: '--scroll-scenario-file root must be a JSON object' };
|
|
971
|
+
}
|
|
972
|
+
return { ok: true, scenario: parsed };
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* CLI/input-boundary-only responsibility, mirroring `loadScrollScenarioFile`
|
|
976
|
+
* exactly: read one local JSON file and validate only the root shape (plain,
|
|
977
|
+
* non-array object) - the file supplies the value of
|
|
978
|
+
* `RawObservationRequest.explicitState` directly (no wrapper field), so
|
|
979
|
+
* there is no root-field allowlist to enforce here. Every state rule
|
|
980
|
+
* (supported dimension keys, label pattern, authenticatedState enum) stays
|
|
981
|
+
* owned by `normalizeRequest()`/`isValidExplicitStateDimensions`, not
|
|
982
|
+
* duplicated here. The file path itself is never returned to the caller
|
|
983
|
+
* beyond this function, so it can never reach the persisted request/artifact.
|
|
984
|
+
*/
|
|
985
|
+
function loadStateFile(filePath) {
|
|
986
|
+
let rawText;
|
|
987
|
+
try {
|
|
988
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
992
|
+
return { ok: false, error: `--state-file could not be read: ${message}` };
|
|
993
|
+
}
|
|
994
|
+
let parsed;
|
|
995
|
+
try {
|
|
996
|
+
parsed = JSON.parse(rawText);
|
|
997
|
+
}
|
|
998
|
+
catch (err) {
|
|
999
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1000
|
+
return { ok: false, error: `--state-file is not valid JSON: ${message}` };
|
|
1001
|
+
}
|
|
1002
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1003
|
+
return { ok: false, error: '--state-file root must be a JSON object' };
|
|
1004
|
+
}
|
|
1005
|
+
return { ok: true, state: parsed };
|
|
1006
|
+
}
|
|
1007
|
+
/** CLI-syntax-only parsing, mirroring `parseObserveArgs`: shape/presence/duplication errors only. Comparison-config semantics stay owned by the existing domain validator. */
|
|
1008
|
+
function parseCompareArgs(argv) {
|
|
1009
|
+
const errors = [];
|
|
1010
|
+
let beforeRoot;
|
|
1011
|
+
let beforeFlagCount = 0;
|
|
1012
|
+
let afterRoot;
|
|
1013
|
+
let afterFlagCount = 0;
|
|
1014
|
+
let outputLocation;
|
|
1015
|
+
let outputFlagCount = 0;
|
|
1016
|
+
let configFilePath;
|
|
1017
|
+
let configFileFlagCount = 0;
|
|
1018
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1019
|
+
const arg = argv[i];
|
|
1020
|
+
switch (arg) {
|
|
1021
|
+
case '--before': {
|
|
1022
|
+
const value = argv[(i += 1)];
|
|
1023
|
+
beforeFlagCount += 1;
|
|
1024
|
+
if (value === undefined) {
|
|
1025
|
+
errors.push('--before requires a path argument');
|
|
1026
|
+
}
|
|
1027
|
+
else if (beforeFlagCount > 1) {
|
|
1028
|
+
errors.push('--before may only be specified once');
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
beforeRoot = value;
|
|
1032
|
+
}
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
case '--after': {
|
|
1036
|
+
const value = argv[(i += 1)];
|
|
1037
|
+
afterFlagCount += 1;
|
|
1038
|
+
if (value === undefined) {
|
|
1039
|
+
errors.push('--after requires a path argument');
|
|
1040
|
+
}
|
|
1041
|
+
else if (afterFlagCount > 1) {
|
|
1042
|
+
errors.push('--after may only be specified once');
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
afterRoot = value;
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
case '--output': {
|
|
1050
|
+
const value = argv[(i += 1)];
|
|
1051
|
+
outputFlagCount += 1;
|
|
1052
|
+
if (value === undefined) {
|
|
1053
|
+
errors.push('--output requires a directory argument');
|
|
1054
|
+
}
|
|
1055
|
+
else if (outputFlagCount > 1) {
|
|
1056
|
+
errors.push('--output may only be specified once');
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
outputLocation = value;
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
case '--config-file': {
|
|
1064
|
+
const value = argv[(i += 1)];
|
|
1065
|
+
configFileFlagCount += 1;
|
|
1066
|
+
if (value === undefined) {
|
|
1067
|
+
errors.push('--config-file requires a file path argument');
|
|
1068
|
+
}
|
|
1069
|
+
else if (configFileFlagCount > 1) {
|
|
1070
|
+
errors.push('--config-file may only be specified once');
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
configFilePath = value;
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
default:
|
|
1078
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (beforeRoot === undefined)
|
|
1082
|
+
errors.push('--before is required');
|
|
1083
|
+
if (afterRoot === undefined)
|
|
1084
|
+
errors.push('--after is required');
|
|
1085
|
+
if (outputLocation === undefined)
|
|
1086
|
+
errors.push('--output is required');
|
|
1087
|
+
if (errors.length > 0)
|
|
1088
|
+
return { ok: false, errors };
|
|
1089
|
+
return {
|
|
1090
|
+
ok: true,
|
|
1091
|
+
beforeRoot: beforeRoot,
|
|
1092
|
+
afterRoot: afterRoot,
|
|
1093
|
+
outputLocation: outputLocation,
|
|
1094
|
+
...(configFilePath === undefined ? {} : { configFilePath }),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* CLI/input-boundary-only responsibility, mirroring `loadScrollScenarioFile`:
|
|
1099
|
+
* read one local JSON file and validate only the root shape this file format
|
|
1100
|
+
* owns (plain, non-array object) - the file supplies the value of
|
|
1101
|
+
* `ComparisonConfig` directly (no wrapper field). Every semantic rule
|
|
1102
|
+
* (geometry tolerance bounds, dependency property/direction vocabulary,
|
|
1103
|
+
* dependency source/provenance) stays owned by the existing comparison
|
|
1104
|
+
* domain validator (`isValidComparisonConfig`, invoked inside
|
|
1105
|
+
* `compareObservations`), not duplicated here. The file path itself is
|
|
1106
|
+
* never returned to the caller beyond this function, so it can never reach
|
|
1107
|
+
* the persisted comparison artifact or its request identity.
|
|
1108
|
+
*/
|
|
1109
|
+
function loadComparisonConfigFile(filePath) {
|
|
1110
|
+
let rawText;
|
|
1111
|
+
try {
|
|
1112
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1116
|
+
return { ok: false, error: `--config-file could not be read: ${message}` };
|
|
1117
|
+
}
|
|
1118
|
+
let parsed;
|
|
1119
|
+
try {
|
|
1120
|
+
parsed = JSON.parse(rawText);
|
|
1121
|
+
}
|
|
1122
|
+
catch (err) {
|
|
1123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1124
|
+
return { ok: false, error: `--config-file is not valid JSON: ${message}` };
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1127
|
+
return { ok: false, error: '--config-file root must be a JSON object' };
|
|
1128
|
+
}
|
|
1129
|
+
return { ok: true, config: parsed };
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* CLI/input-boundary-only responsibility, mirroring `loadComparisonConfigFile`:
|
|
1133
|
+
* read one local JSON file and validate only the root shape this file format
|
|
1134
|
+
* owns (plain, non-array object) - the file supplies the raw contract value
|
|
1135
|
+
* directly (no wrapper field). Every semantic/structural rule (artifact
|
|
1136
|
+
* kind, schema version, contract class, clause shape, authored category
|
|
1137
|
+
* vocabulary) stays owned by the existing frozen domain validators
|
|
1138
|
+
* (`isValidPersistentBaselineContract`/`isValidPerChangeContract`, invoked
|
|
1139
|
+
* inside the application layer), never duplicated here. The file path
|
|
1140
|
+
* itself is never returned to the caller beyond this function, so it can
|
|
1141
|
+
* never reach a persisted artifact or its identity.
|
|
1142
|
+
*/
|
|
1143
|
+
function loadContractFile(filePath, flagLabel) {
|
|
1144
|
+
let rawText;
|
|
1145
|
+
try {
|
|
1146
|
+
rawText = readFileSync(filePath, 'utf8');
|
|
1147
|
+
}
|
|
1148
|
+
catch (err) {
|
|
1149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1150
|
+
return { ok: false, error: `${flagLabel} could not be read: ${message}` };
|
|
1151
|
+
}
|
|
1152
|
+
let parsed;
|
|
1153
|
+
try {
|
|
1154
|
+
parsed = JSON.parse(rawText);
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1158
|
+
return { ok: false, error: `${flagLabel} is not valid JSON: ${message}` };
|
|
1159
|
+
}
|
|
1160
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1161
|
+
return { ok: false, error: `${flagLabel} root must be a JSON object` };
|
|
1162
|
+
}
|
|
1163
|
+
return { ok: true, contract: parsed };
|
|
1164
|
+
}
|
|
1165
|
+
/** CLI-syntax-only parsing, mirroring `parseCompareArgs`. */
|
|
1166
|
+
function parseApproveBaselineArgs(argv) {
|
|
1167
|
+
const errors = [];
|
|
1168
|
+
let observationRoot;
|
|
1169
|
+
let observationFlagCount = 0;
|
|
1170
|
+
let contractFilePath;
|
|
1171
|
+
let contractFileFlagCount = 0;
|
|
1172
|
+
let outputLocation;
|
|
1173
|
+
let outputFlagCount = 0;
|
|
1174
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1175
|
+
const arg = argv[i];
|
|
1176
|
+
switch (arg) {
|
|
1177
|
+
case '--observation': {
|
|
1178
|
+
const value = argv[(i += 1)];
|
|
1179
|
+
observationFlagCount += 1;
|
|
1180
|
+
if (value === undefined)
|
|
1181
|
+
errors.push('--observation requires a path argument');
|
|
1182
|
+
else if (observationFlagCount > 1)
|
|
1183
|
+
errors.push('--observation may only be specified once');
|
|
1184
|
+
else
|
|
1185
|
+
observationRoot = value;
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
case '--contract-file': {
|
|
1189
|
+
const value = argv[(i += 1)];
|
|
1190
|
+
contractFileFlagCount += 1;
|
|
1191
|
+
if (value === undefined)
|
|
1192
|
+
errors.push('--contract-file requires a file path argument');
|
|
1193
|
+
else if (contractFileFlagCount > 1)
|
|
1194
|
+
errors.push('--contract-file may only be specified once');
|
|
1195
|
+
else
|
|
1196
|
+
contractFilePath = value;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
case '--output': {
|
|
1200
|
+
const value = argv[(i += 1)];
|
|
1201
|
+
outputFlagCount += 1;
|
|
1202
|
+
if (value === undefined)
|
|
1203
|
+
errors.push('--output requires a directory argument');
|
|
1204
|
+
else if (outputFlagCount > 1)
|
|
1205
|
+
errors.push('--output may only be specified once');
|
|
1206
|
+
else
|
|
1207
|
+
outputLocation = value;
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
default:
|
|
1211
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (observationRoot === undefined)
|
|
1215
|
+
errors.push('--observation is required');
|
|
1216
|
+
if (contractFilePath === undefined)
|
|
1217
|
+
errors.push('--contract-file is required');
|
|
1218
|
+
if (outputLocation === undefined)
|
|
1219
|
+
errors.push('--output is required');
|
|
1220
|
+
if (errors.length > 0)
|
|
1221
|
+
return { ok: false, errors };
|
|
1222
|
+
return { ok: true, observationRoot: observationRoot, contractFilePath: contractFilePath, outputLocation: outputLocation };
|
|
1223
|
+
}
|
|
1224
|
+
/** CLI-syntax-only parsing, mirroring `parseCompareArgs`. */
|
|
1225
|
+
function parseSaveChangeContractArgs(argv) {
|
|
1226
|
+
const errors = [];
|
|
1227
|
+
let contractFilePath;
|
|
1228
|
+
let contractFileFlagCount = 0;
|
|
1229
|
+
let outputLocation;
|
|
1230
|
+
let outputFlagCount = 0;
|
|
1231
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1232
|
+
const arg = argv[i];
|
|
1233
|
+
switch (arg) {
|
|
1234
|
+
case '--contract-file': {
|
|
1235
|
+
const value = argv[(i += 1)];
|
|
1236
|
+
contractFileFlagCount += 1;
|
|
1237
|
+
if (value === undefined)
|
|
1238
|
+
errors.push('--contract-file requires a file path argument');
|
|
1239
|
+
else if (contractFileFlagCount > 1)
|
|
1240
|
+
errors.push('--contract-file may only be specified once');
|
|
1241
|
+
else
|
|
1242
|
+
contractFilePath = value;
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
case '--output': {
|
|
1246
|
+
const value = argv[(i += 1)];
|
|
1247
|
+
outputFlagCount += 1;
|
|
1248
|
+
if (value === undefined)
|
|
1249
|
+
errors.push('--output requires a directory argument');
|
|
1250
|
+
else if (outputFlagCount > 1)
|
|
1251
|
+
errors.push('--output may only be specified once');
|
|
1252
|
+
else
|
|
1253
|
+
outputLocation = value;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
default:
|
|
1257
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (contractFilePath === undefined)
|
|
1261
|
+
errors.push('--contract-file is required');
|
|
1262
|
+
if (outputLocation === undefined)
|
|
1263
|
+
errors.push('--output is required');
|
|
1264
|
+
if (errors.length > 0)
|
|
1265
|
+
return { ok: false, errors };
|
|
1266
|
+
return { ok: true, contractFilePath: contractFilePath, outputLocation: outputLocation };
|
|
1267
|
+
}
|
|
1268
|
+
/** CLI-syntax-only parsing. `--enforce` is a boolean switch (no value); repeating it is harmless (idempotent), matching a boolean flag's natural semantics rather than the "may only be specified once" policy used for single-value flags. */
|
|
1269
|
+
function parseEvaluateContractArgs(argv) {
|
|
1270
|
+
const errors = [];
|
|
1271
|
+
let beforeRoot;
|
|
1272
|
+
let beforeFlagCount = 0;
|
|
1273
|
+
let afterRoot;
|
|
1274
|
+
let afterFlagCount = 0;
|
|
1275
|
+
let comparisonRoot;
|
|
1276
|
+
let comparisonFlagCount = 0;
|
|
1277
|
+
let baselineRoot;
|
|
1278
|
+
let baselineFlagCount = 0;
|
|
1279
|
+
let changeRoot;
|
|
1280
|
+
let changeFlagCount = 0;
|
|
1281
|
+
let outputLocation;
|
|
1282
|
+
let outputFlagCount = 0;
|
|
1283
|
+
let enforce = false;
|
|
1284
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1285
|
+
const arg = argv[i];
|
|
1286
|
+
switch (arg) {
|
|
1287
|
+
case '--before': {
|
|
1288
|
+
const value = argv[(i += 1)];
|
|
1289
|
+
beforeFlagCount += 1;
|
|
1290
|
+
if (value === undefined)
|
|
1291
|
+
errors.push('--before requires a path argument');
|
|
1292
|
+
else if (beforeFlagCount > 1)
|
|
1293
|
+
errors.push('--before may only be specified once');
|
|
1294
|
+
else
|
|
1295
|
+
beforeRoot = value;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
case '--after': {
|
|
1299
|
+
const value = argv[(i += 1)];
|
|
1300
|
+
afterFlagCount += 1;
|
|
1301
|
+
if (value === undefined)
|
|
1302
|
+
errors.push('--after requires a path argument');
|
|
1303
|
+
else if (afterFlagCount > 1)
|
|
1304
|
+
errors.push('--after may only be specified once');
|
|
1305
|
+
else
|
|
1306
|
+
afterRoot = value;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
case '--comparison': {
|
|
1310
|
+
const value = argv[(i += 1)];
|
|
1311
|
+
comparisonFlagCount += 1;
|
|
1312
|
+
if (value === undefined)
|
|
1313
|
+
errors.push('--comparison requires a path argument');
|
|
1314
|
+
else if (comparisonFlagCount > 1)
|
|
1315
|
+
errors.push('--comparison may only be specified once');
|
|
1316
|
+
else
|
|
1317
|
+
comparisonRoot = value;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
case '--baseline': {
|
|
1321
|
+
const value = argv[(i += 1)];
|
|
1322
|
+
baselineFlagCount += 1;
|
|
1323
|
+
if (value === undefined)
|
|
1324
|
+
errors.push('--baseline requires a path argument');
|
|
1325
|
+
else if (baselineFlagCount > 1)
|
|
1326
|
+
errors.push('--baseline may only be specified once');
|
|
1327
|
+
else
|
|
1328
|
+
baselineRoot = value;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case '--change': {
|
|
1332
|
+
const value = argv[(i += 1)];
|
|
1333
|
+
changeFlagCount += 1;
|
|
1334
|
+
if (value === undefined)
|
|
1335
|
+
errors.push('--change requires a path argument');
|
|
1336
|
+
else if (changeFlagCount > 1)
|
|
1337
|
+
errors.push('--change may only be specified once');
|
|
1338
|
+
else
|
|
1339
|
+
changeRoot = value;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
case '--output': {
|
|
1343
|
+
const value = argv[(i += 1)];
|
|
1344
|
+
outputFlagCount += 1;
|
|
1345
|
+
if (value === undefined)
|
|
1346
|
+
errors.push('--output requires a directory argument');
|
|
1347
|
+
else if (outputFlagCount > 1)
|
|
1348
|
+
errors.push('--output may only be specified once');
|
|
1349
|
+
else
|
|
1350
|
+
outputLocation = value;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
case '--enforce':
|
|
1354
|
+
enforce = true;
|
|
1355
|
+
break;
|
|
1356
|
+
default:
|
|
1357
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (beforeRoot === undefined)
|
|
1361
|
+
errors.push('--before is required');
|
|
1362
|
+
if (afterRoot === undefined)
|
|
1363
|
+
errors.push('--after is required');
|
|
1364
|
+
if (comparisonRoot === undefined)
|
|
1365
|
+
errors.push('--comparison is required');
|
|
1366
|
+
if (baselineRoot === undefined)
|
|
1367
|
+
errors.push('--baseline is required');
|
|
1368
|
+
if (changeRoot === undefined)
|
|
1369
|
+
errors.push('--change is required');
|
|
1370
|
+
if (outputLocation === undefined)
|
|
1371
|
+
errors.push('--output is required');
|
|
1372
|
+
if (errors.length > 0)
|
|
1373
|
+
return { ok: false, errors };
|
|
1374
|
+
return {
|
|
1375
|
+
ok: true,
|
|
1376
|
+
beforeRoot: beforeRoot,
|
|
1377
|
+
afterRoot: afterRoot,
|
|
1378
|
+
comparisonRoot: comparisonRoot,
|
|
1379
|
+
baselineRoot: baselineRoot,
|
|
1380
|
+
changeRoot: changeRoot,
|
|
1381
|
+
outputLocation: outputLocation,
|
|
1382
|
+
enforce,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
/** CLI-syntax-only parsing, mirroring `parseApproveBaselineArgs`. The image file path is the one positional argument. */
|
|
1386
|
+
function parseImportReferenceArgs(argv) {
|
|
1387
|
+
const errors = [];
|
|
1388
|
+
let imageFilePath;
|
|
1389
|
+
let outputLocation;
|
|
1390
|
+
let outputFlagCount = 0;
|
|
1391
|
+
let label;
|
|
1392
|
+
let labelFlagCount = 0;
|
|
1393
|
+
let supersedesReferenceRoot;
|
|
1394
|
+
let supersedesFlagCount = 0;
|
|
1395
|
+
let regionsFilePath;
|
|
1396
|
+
let regionsFileFlagCount = 0;
|
|
1397
|
+
let requirementsFilePath;
|
|
1398
|
+
let requirementsFileFlagCount = 0;
|
|
1399
|
+
let applicabilityFilePath;
|
|
1400
|
+
let applicabilityFileFlagCount = 0;
|
|
1401
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1402
|
+
const arg = argv[i];
|
|
1403
|
+
switch (arg) {
|
|
1404
|
+
case '--output': {
|
|
1405
|
+
const value = argv[(i += 1)];
|
|
1406
|
+
outputFlagCount += 1;
|
|
1407
|
+
if (value === undefined)
|
|
1408
|
+
errors.push('--output requires a directory argument');
|
|
1409
|
+
else if (outputFlagCount > 1)
|
|
1410
|
+
errors.push('--output may only be specified once');
|
|
1411
|
+
else
|
|
1412
|
+
outputLocation = value;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
case '--label': {
|
|
1416
|
+
const value = argv[(i += 1)];
|
|
1417
|
+
labelFlagCount += 1;
|
|
1418
|
+
if (value === undefined)
|
|
1419
|
+
errors.push('--label requires a text argument');
|
|
1420
|
+
else if (labelFlagCount > 1)
|
|
1421
|
+
errors.push('--label may only be specified once');
|
|
1422
|
+
else
|
|
1423
|
+
label = value;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
case '--supersedes': {
|
|
1427
|
+
const value = argv[(i += 1)];
|
|
1428
|
+
supersedesFlagCount += 1;
|
|
1429
|
+
if (value === undefined)
|
|
1430
|
+
errors.push('--supersedes requires a path argument');
|
|
1431
|
+
else if (supersedesFlagCount > 1)
|
|
1432
|
+
errors.push('--supersedes may only be specified once');
|
|
1433
|
+
else
|
|
1434
|
+
supersedesReferenceRoot = value;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
case '--regions-file': {
|
|
1438
|
+
const value = argv[(i += 1)];
|
|
1439
|
+
regionsFileFlagCount += 1;
|
|
1440
|
+
if (value === undefined)
|
|
1441
|
+
errors.push('--regions-file requires a file path argument');
|
|
1442
|
+
else if (regionsFileFlagCount > 1)
|
|
1443
|
+
errors.push('--regions-file may only be specified once');
|
|
1444
|
+
else
|
|
1445
|
+
regionsFilePath = value;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
case '--requirements-file': {
|
|
1449
|
+
const value = argv[(i += 1)];
|
|
1450
|
+
requirementsFileFlagCount += 1;
|
|
1451
|
+
if (value === undefined)
|
|
1452
|
+
errors.push('--requirements-file requires a file path argument');
|
|
1453
|
+
else if (requirementsFileFlagCount > 1)
|
|
1454
|
+
errors.push('--requirements-file may only be specified once');
|
|
1455
|
+
else
|
|
1456
|
+
requirementsFilePath = value;
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
case '--applicability-file': {
|
|
1460
|
+
const value = argv[(i += 1)];
|
|
1461
|
+
applicabilityFileFlagCount += 1;
|
|
1462
|
+
if (value === undefined)
|
|
1463
|
+
errors.push('--applicability-file requires a file path argument');
|
|
1464
|
+
else if (applicabilityFileFlagCount > 1)
|
|
1465
|
+
errors.push('--applicability-file may only be specified once');
|
|
1466
|
+
else
|
|
1467
|
+
applicabilityFilePath = value;
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
default:
|
|
1471
|
+
if (arg === undefined)
|
|
1472
|
+
break;
|
|
1473
|
+
if (arg.startsWith('--'))
|
|
1474
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1475
|
+
else if (imageFilePath !== undefined)
|
|
1476
|
+
errors.push('only one image-file argument may be given');
|
|
1477
|
+
else
|
|
1478
|
+
imageFilePath = arg;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (imageFilePath === undefined)
|
|
1482
|
+
errors.push('an image-file argument is required');
|
|
1483
|
+
if (outputLocation === undefined)
|
|
1484
|
+
errors.push('--output is required');
|
|
1485
|
+
if (errors.length > 0)
|
|
1486
|
+
return { ok: false, errors };
|
|
1487
|
+
return {
|
|
1488
|
+
ok: true,
|
|
1489
|
+
imageFilePath: imageFilePath,
|
|
1490
|
+
outputLocation: outputLocation,
|
|
1491
|
+
...(label === undefined ? {} : { label }),
|
|
1492
|
+
...(supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot }),
|
|
1493
|
+
...(regionsFilePath === undefined ? {} : { regionsFilePath }),
|
|
1494
|
+
...(requirementsFilePath === undefined ? {} : { requirementsFilePath }),
|
|
1495
|
+
...(applicabilityFilePath === undefined ? {} : { applicabilityFilePath }),
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
/** CLI-syntax-only parsing, mirroring `parseApproveBaselineArgs`. */
|
|
1499
|
+
function parseApproveReferenceArgs(argv) {
|
|
1500
|
+
const errors = [];
|
|
1501
|
+
let referenceRoot;
|
|
1502
|
+
let referenceFlagCount = 0;
|
|
1503
|
+
let outputLocation;
|
|
1504
|
+
let outputFlagCount = 0;
|
|
1505
|
+
let supersedesReferenceRoot;
|
|
1506
|
+
let supersedesFlagCount = 0;
|
|
1507
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1508
|
+
const arg = argv[i];
|
|
1509
|
+
switch (arg) {
|
|
1510
|
+
case '--reference': {
|
|
1511
|
+
const value = argv[(i += 1)];
|
|
1512
|
+
referenceFlagCount += 1;
|
|
1513
|
+
if (value === undefined)
|
|
1514
|
+
errors.push('--reference requires a path argument');
|
|
1515
|
+
else if (referenceFlagCount > 1)
|
|
1516
|
+
errors.push('--reference may only be specified once');
|
|
1517
|
+
else
|
|
1518
|
+
referenceRoot = value;
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
case '--output': {
|
|
1522
|
+
const value = argv[(i += 1)];
|
|
1523
|
+
outputFlagCount += 1;
|
|
1524
|
+
if (value === undefined)
|
|
1525
|
+
errors.push('--output requires a directory argument');
|
|
1526
|
+
else if (outputFlagCount > 1)
|
|
1527
|
+
errors.push('--output may only be specified once');
|
|
1528
|
+
else
|
|
1529
|
+
outputLocation = value;
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
case '--supersedes': {
|
|
1533
|
+
const value = argv[(i += 1)];
|
|
1534
|
+
supersedesFlagCount += 1;
|
|
1535
|
+
if (value === undefined)
|
|
1536
|
+
errors.push('--supersedes requires a path argument');
|
|
1537
|
+
else if (supersedesFlagCount > 1)
|
|
1538
|
+
errors.push('--supersedes may only be specified once');
|
|
1539
|
+
else
|
|
1540
|
+
supersedesReferenceRoot = value;
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
default:
|
|
1544
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (referenceRoot === undefined)
|
|
1548
|
+
errors.push('--reference is required');
|
|
1549
|
+
if (outputLocation === undefined)
|
|
1550
|
+
errors.push('--output is required');
|
|
1551
|
+
if (errors.length > 0)
|
|
1552
|
+
return { ok: false, errors };
|
|
1553
|
+
return {
|
|
1554
|
+
ok: true,
|
|
1555
|
+
referenceRoot: referenceRoot,
|
|
1556
|
+
outputLocation: outputLocation,
|
|
1557
|
+
...(supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot }),
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
/** CLI-syntax-only parsing, mirroring `parseEvaluateContractArgs`'s `--enforce` handling exactly. */
|
|
1561
|
+
function parseEvaluateReferenceFidelityArgs(argv) {
|
|
1562
|
+
const errors = [];
|
|
1563
|
+
let referenceRoot;
|
|
1564
|
+
let referenceFlagCount = 0;
|
|
1565
|
+
let candidateRoot;
|
|
1566
|
+
let candidateFlagCount = 0;
|
|
1567
|
+
let bindingsFilePath;
|
|
1568
|
+
let bindingsFileFlagCount = 0;
|
|
1569
|
+
let enforce = false;
|
|
1570
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1571
|
+
const arg = argv[i];
|
|
1572
|
+
switch (arg) {
|
|
1573
|
+
case '--reference': {
|
|
1574
|
+
const value = argv[(i += 1)];
|
|
1575
|
+
referenceFlagCount += 1;
|
|
1576
|
+
if (value === undefined)
|
|
1577
|
+
errors.push('--reference requires a path argument');
|
|
1578
|
+
else if (referenceFlagCount > 1)
|
|
1579
|
+
errors.push('--reference may only be specified once');
|
|
1580
|
+
else
|
|
1581
|
+
referenceRoot = value;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
case '--candidate': {
|
|
1585
|
+
const value = argv[(i += 1)];
|
|
1586
|
+
candidateFlagCount += 1;
|
|
1587
|
+
if (value === undefined)
|
|
1588
|
+
errors.push('--candidate requires a path argument');
|
|
1589
|
+
else if (candidateFlagCount > 1)
|
|
1590
|
+
errors.push('--candidate may only be specified once');
|
|
1591
|
+
else
|
|
1592
|
+
candidateRoot = value;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
case '--bindings-file': {
|
|
1596
|
+
const value = argv[(i += 1)];
|
|
1597
|
+
bindingsFileFlagCount += 1;
|
|
1598
|
+
if (value === undefined)
|
|
1599
|
+
errors.push('--bindings-file requires a file path argument');
|
|
1600
|
+
else if (bindingsFileFlagCount > 1)
|
|
1601
|
+
errors.push('--bindings-file may only be specified once');
|
|
1602
|
+
else
|
|
1603
|
+
bindingsFilePath = value;
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
case '--enforce':
|
|
1607
|
+
enforce = true;
|
|
1608
|
+
break;
|
|
1609
|
+
default:
|
|
1610
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (referenceRoot === undefined)
|
|
1614
|
+
errors.push('--reference is required');
|
|
1615
|
+
if (candidateRoot === undefined)
|
|
1616
|
+
errors.push('--candidate is required');
|
|
1617
|
+
if (errors.length > 0)
|
|
1618
|
+
return { ok: false, errors };
|
|
1619
|
+
return {
|
|
1620
|
+
ok: true,
|
|
1621
|
+
referenceRoot: referenceRoot,
|
|
1622
|
+
candidateRoot: candidateRoot,
|
|
1623
|
+
...(bindingsFilePath === undefined ? {} : { bindingsFilePath }),
|
|
1624
|
+
enforce,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function formatDiagnostic(diagnostic) {
|
|
1628
|
+
const target = diagnostic.targetName === undefined ? '' : ` (target: ${diagnostic.targetName})`;
|
|
1629
|
+
return `[${diagnostic.code}] ${diagnostic.message}${target}`;
|
|
1630
|
+
}
|
|
1631
|
+
/** Completion states that must never be reported as a successful process exit, even though they may still have a persisted artifact. */
|
|
1632
|
+
const NON_SUCCESS_COMPLETION_STATES = new Set(['fatal', 'invalid-request']);
|
|
1633
|
+
async function runObserveCommand(argv, io) {
|
|
1634
|
+
if (argv.includes('--help')) {
|
|
1635
|
+
io.stdout(OBSERVE_HELP);
|
|
1636
|
+
return 0;
|
|
1637
|
+
}
|
|
1638
|
+
const parsedArgs = parseObserveArgs(argv);
|
|
1639
|
+
if (!parsedArgs.ok) {
|
|
1640
|
+
for (const error of parsedArgs.errors)
|
|
1641
|
+
io.stderr(`error: ${error}\n`);
|
|
1642
|
+
io.stderr(OBSERVE_HELP);
|
|
1643
|
+
return 1;
|
|
1644
|
+
}
|
|
1645
|
+
let raw = parsedArgs.raw;
|
|
1646
|
+
if (parsedArgs.targetsFilePath !== undefined) {
|
|
1647
|
+
const loaded = loadTargetsFile(parsedArgs.targetsFilePath);
|
|
1648
|
+
if (!loaded.ok) {
|
|
1649
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1650
|
+
io.stderr(OBSERVE_HELP);
|
|
1651
|
+
return 1;
|
|
1652
|
+
}
|
|
1653
|
+
raw = { ...raw, targets: loaded.targets };
|
|
1654
|
+
}
|
|
1655
|
+
if (parsedArgs.scrollScenarioFilePath !== undefined) {
|
|
1656
|
+
const loaded = loadScrollScenarioFile(parsedArgs.scrollScenarioFilePath);
|
|
1657
|
+
if (!loaded.ok) {
|
|
1658
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1659
|
+
io.stderr(OBSERVE_HELP);
|
|
1660
|
+
return 1;
|
|
1661
|
+
}
|
|
1662
|
+
raw = { ...raw, scrollScenario: loaded.scenario };
|
|
1663
|
+
}
|
|
1664
|
+
if (parsedArgs.stateFilePath !== undefined) {
|
|
1665
|
+
const loaded = loadStateFile(parsedArgs.stateFilePath);
|
|
1666
|
+
if (!loaded.ok) {
|
|
1667
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1668
|
+
io.stderr(OBSERVE_HELP);
|
|
1669
|
+
return 1;
|
|
1670
|
+
}
|
|
1671
|
+
raw = { ...raw, explicitState: loaded.state };
|
|
1672
|
+
}
|
|
1673
|
+
const normalized = normalizeRequest(raw);
|
|
1674
|
+
if (!normalized.ok) {
|
|
1675
|
+
for (const diagnostic of normalized.diagnostics)
|
|
1676
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1677
|
+
return 1;
|
|
1678
|
+
}
|
|
1679
|
+
// Exactly one application observation attempt: one browser capture, persisted at most once.
|
|
1680
|
+
const result = await observe(normalized.request);
|
|
1681
|
+
if (!result.ok) {
|
|
1682
|
+
for (const diagnostic of result.diagnostics)
|
|
1683
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1684
|
+
return 1;
|
|
1685
|
+
}
|
|
1686
|
+
io.stdout(`Observation: ${result.observationId}\n`);
|
|
1687
|
+
io.stdout(`State: ${result.completion.state}\n`);
|
|
1688
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1689
|
+
io.stdout(`Targets: ${result.targetCount}\n`);
|
|
1690
|
+
io.stdout(`Diagnostics: ${result.diagnostics.length}\n`);
|
|
1691
|
+
return NON_SUCCESS_COMPLETION_STATES.has(result.completion.state) ? 1 : 0;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Thin orchestration only: parse args, optionally load a config file, then
|
|
1695
|
+
* delegate to the existing `compareAndPersistFromArtifactRoots` application
|
|
1696
|
+
* function exactly once. No comparability/geometry/relationship/dependency
|
|
1697
|
+
* logic lives here - see `src/domain/comparisonEngine.ts`. `incomparable` is
|
|
1698
|
+
* a successful comparison outcome (the operation determined the two
|
|
1699
|
+
* observations should not be treated as equivalent frontend states), so it
|
|
1700
|
+
* exits 0 exactly like `comparable`/`comparable-with-warnings`; only a
|
|
1701
|
+
* genuine parse/read/domain/persistence failure exits nonzero.
|
|
1702
|
+
*/
|
|
1703
|
+
async function runCompareCommand(argv, io) {
|
|
1704
|
+
if (argv.includes('--help')) {
|
|
1705
|
+
io.stdout(COMPARE_HELP);
|
|
1706
|
+
return 0;
|
|
1707
|
+
}
|
|
1708
|
+
const parsedArgs = parseCompareArgs(argv);
|
|
1709
|
+
if (!parsedArgs.ok) {
|
|
1710
|
+
for (const error of parsedArgs.errors)
|
|
1711
|
+
io.stderr(`error: ${error}\n`);
|
|
1712
|
+
io.stderr(COMPARE_HELP);
|
|
1713
|
+
return 1;
|
|
1714
|
+
}
|
|
1715
|
+
let config;
|
|
1716
|
+
if (parsedArgs.configFilePath !== undefined) {
|
|
1717
|
+
const loaded = loadComparisonConfigFile(parsedArgs.configFilePath);
|
|
1718
|
+
if (!loaded.ok) {
|
|
1719
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1720
|
+
io.stderr(COMPARE_HELP);
|
|
1721
|
+
return 1;
|
|
1722
|
+
}
|
|
1723
|
+
config = loaded.config;
|
|
1724
|
+
}
|
|
1725
|
+
// Exactly one application comparison attempt: two artifact reads, one pure comparison, persisted at most once.
|
|
1726
|
+
const result = await compareAndPersistFromArtifactRoots(parsedArgs.beforeRoot, parsedArgs.afterRoot, {
|
|
1727
|
+
...(config === undefined ? {} : { config }),
|
|
1728
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1729
|
+
});
|
|
1730
|
+
if (!result.ok) {
|
|
1731
|
+
for (const diagnostic of result.diagnostics)
|
|
1732
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1733
|
+
return 1;
|
|
1734
|
+
}
|
|
1735
|
+
io.stdout(`Comparison: ${result.comparisonId}\n`);
|
|
1736
|
+
io.stdout(`State: ${result.comparability}\n`);
|
|
1737
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1738
|
+
io.stdout(`Differences: ${result.differenceCount}\n`);
|
|
1739
|
+
io.stdout(`Relationship changes: ${result.relationshipChangeCount}\n`);
|
|
1740
|
+
io.stdout(`Diagnostics: ${result.diagnosticsCount}\n`);
|
|
1741
|
+
return 0;
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Thin orchestration only: parse args, load the raw contract JSON file, then
|
|
1745
|
+
* delegate to the existing `approveAndPersistBaseline` application function
|
|
1746
|
+
* exactly once. No contract/coherence validation lives here - see
|
|
1747
|
+
* `src/application/frontendContractPersistenceService.ts`. This is the only
|
|
1748
|
+
* command in the observer that approves a baseline.
|
|
1749
|
+
*/
|
|
1750
|
+
async function runApproveBaselineCommand(argv, io) {
|
|
1751
|
+
if (argv.includes('--help')) {
|
|
1752
|
+
io.stdout(APPROVE_BASELINE_HELP);
|
|
1753
|
+
return 0;
|
|
1754
|
+
}
|
|
1755
|
+
const parsedArgs = parseApproveBaselineArgs(argv);
|
|
1756
|
+
if (!parsedArgs.ok) {
|
|
1757
|
+
for (const error of parsedArgs.errors)
|
|
1758
|
+
io.stderr(`error: ${error}\n`);
|
|
1759
|
+
io.stderr(APPROVE_BASELINE_HELP);
|
|
1760
|
+
return 1;
|
|
1761
|
+
}
|
|
1762
|
+
const loaded = loadContractFile(parsedArgs.contractFilePath, '--contract-file');
|
|
1763
|
+
if (!loaded.ok) {
|
|
1764
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1765
|
+
io.stderr(APPROVE_BASELINE_HELP);
|
|
1766
|
+
return 1;
|
|
1767
|
+
}
|
|
1768
|
+
// Exactly one application approval attempt: one observation read, one coherence check, persisted at most once.
|
|
1769
|
+
const result = await approveAndPersistBaseline(loaded.contract, parsedArgs.observationRoot, { outputLocation: parsedArgs.outputLocation });
|
|
1770
|
+
if (!result.ok) {
|
|
1771
|
+
for (const diagnostic of result.diagnostics)
|
|
1772
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1773
|
+
return 1;
|
|
1774
|
+
}
|
|
1775
|
+
io.stdout(`Baseline: ${result.baselineId}\n`);
|
|
1776
|
+
io.stdout(`State: approved\n`);
|
|
1777
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1778
|
+
io.stdout(`Clauses: ${result.clauseCount}\n`);
|
|
1779
|
+
io.stdout(`Supersedes: ${result.supersedesBaselineId ?? 'none'}\n`);
|
|
1780
|
+
return 0;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Thin orchestration only: parse args, load the raw contract JSON file, then
|
|
1784
|
+
* delegate to the existing `persistPerChangeContract` application function
|
|
1785
|
+
* exactly once. Persistence, not approval.
|
|
1786
|
+
*/
|
|
1787
|
+
async function runSaveChangeContractCommand(argv, io) {
|
|
1788
|
+
if (argv.includes('--help')) {
|
|
1789
|
+
io.stdout(SAVE_CHANGE_CONTRACT_HELP);
|
|
1790
|
+
return 0;
|
|
1791
|
+
}
|
|
1792
|
+
const parsedArgs = parseSaveChangeContractArgs(argv);
|
|
1793
|
+
if (!parsedArgs.ok) {
|
|
1794
|
+
for (const error of parsedArgs.errors)
|
|
1795
|
+
io.stderr(`error: ${error}\n`);
|
|
1796
|
+
io.stderr(SAVE_CHANGE_CONTRACT_HELP);
|
|
1797
|
+
return 1;
|
|
1798
|
+
}
|
|
1799
|
+
const loaded = loadContractFile(parsedArgs.contractFilePath, '--contract-file');
|
|
1800
|
+
if (!loaded.ok) {
|
|
1801
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1802
|
+
io.stderr(SAVE_CHANGE_CONTRACT_HELP);
|
|
1803
|
+
return 1;
|
|
1804
|
+
}
|
|
1805
|
+
// Exactly one application persistence attempt.
|
|
1806
|
+
const result = await persistPerChangeContract(loaded.contract, { outputLocation: parsedArgs.outputLocation });
|
|
1807
|
+
if (!result.ok) {
|
|
1808
|
+
for (const diagnostic of result.diagnostics)
|
|
1809
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1810
|
+
return 1;
|
|
1811
|
+
}
|
|
1812
|
+
io.stdout(`Change contract: ${result.contractId}\n`);
|
|
1813
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1814
|
+
io.stdout(`Clauses: ${result.clauseCount}\n`);
|
|
1815
|
+
io.stdout(`Supersedes baseline clauses: ${result.supersedesBaselineClauseCount}\n`);
|
|
1816
|
+
return 0;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Thin orchestration only: parse args, then delegate to the existing
|
|
1820
|
+
* `evaluateAndPersistFromArtifactRoots` application function exactly once.
|
|
1821
|
+
* No evaluation/tolerance/conflict/unexpected-classification logic lives
|
|
1822
|
+
* here - see `src/domain/frontendContractEvaluation.ts`. `--enforce` is
|
|
1823
|
+
* applied only after the evaluation has already been constructed and
|
|
1824
|
+
* persisted: it selects the process exit status for an already-final FAIL
|
|
1825
|
+
* result and never affects evaluation identity, contents, or persistence. A
|
|
1826
|
+
* FAIL verdict is a successful, persisted evaluation outcome (a found
|
|
1827
|
+
* regression), never treated as a construction/persistence failure.
|
|
1828
|
+
*/
|
|
1829
|
+
async function runEvaluateContractCommand(argv, io) {
|
|
1830
|
+
if (argv.includes('--help')) {
|
|
1831
|
+
io.stdout(EVALUATE_CONTRACT_HELP);
|
|
1832
|
+
return 0;
|
|
1833
|
+
}
|
|
1834
|
+
const parsedArgs = parseEvaluateContractArgs(argv);
|
|
1835
|
+
if (!parsedArgs.ok) {
|
|
1836
|
+
for (const error of parsedArgs.errors)
|
|
1837
|
+
io.stderr(`error: ${error}\n`);
|
|
1838
|
+
io.stderr(EVALUATE_CONTRACT_HELP);
|
|
1839
|
+
return 1;
|
|
1840
|
+
}
|
|
1841
|
+
// Exactly one application evaluation attempt: reads before/after/comparison/baseline/change once each,
|
|
1842
|
+
// calls the canonical evaluator exactly once, and persists exactly one evaluation artifact.
|
|
1843
|
+
const result = await evaluateAndPersistFromArtifactRoots(parsedArgs.beforeRoot, parsedArgs.afterRoot, parsedArgs.comparisonRoot, parsedArgs.baselineRoot, parsedArgs.changeRoot, {
|
|
1844
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1845
|
+
});
|
|
1846
|
+
if (!result.ok) {
|
|
1847
|
+
for (const diagnostic of result.diagnostics)
|
|
1848
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1849
|
+
return 1;
|
|
1850
|
+
}
|
|
1851
|
+
io.stdout(`Evaluation: ${result.evaluationId}\n`);
|
|
1852
|
+
io.stdout(`Verdict: ${result.overallVerdict}\n`);
|
|
1853
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1854
|
+
io.stdout(`Clauses: ${result.clauseResultCount}\n`);
|
|
1855
|
+
io.stdout(`Unexpected: ${result.unexpectedChangeCount}\n`);
|
|
1856
|
+
io.stdout(`Enforced: ${parsedArgs.enforce ? 'yes' : 'no'}\n`);
|
|
1857
|
+
if (parsedArgs.enforce && result.overallVerdict === 'FAIL')
|
|
1858
|
+
return 1;
|
|
1859
|
+
return 0;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Thin orchestration only: parse args, read the local image file's raw
|
|
1863
|
+
* bytes, then delegate to the existing `importExternalReference` application
|
|
1864
|
+
* function exactly once. No format/dimension validation lives here - see
|
|
1865
|
+
* `src/domain/externalReferenceImage.ts`.
|
|
1866
|
+
*/
|
|
1867
|
+
async function runImportReferenceCommand(argv, io) {
|
|
1868
|
+
if (argv.includes('--help')) {
|
|
1869
|
+
io.stdout(IMPORT_REFERENCE_HELP);
|
|
1870
|
+
return 0;
|
|
1871
|
+
}
|
|
1872
|
+
const parsedArgs = parseImportReferenceArgs(argv);
|
|
1873
|
+
if (!parsedArgs.ok) {
|
|
1874
|
+
for (const error of parsedArgs.errors)
|
|
1875
|
+
io.stderr(`error: ${error}\n`);
|
|
1876
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1877
|
+
return 1;
|
|
1878
|
+
}
|
|
1879
|
+
let imageBytes;
|
|
1880
|
+
try {
|
|
1881
|
+
imageBytes = readFileSync(parsedArgs.imageFilePath);
|
|
1882
|
+
}
|
|
1883
|
+
catch (err) {
|
|
1884
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1885
|
+
io.stderr(`error: could not read image file "${parsedArgs.imageFilePath}": ${message}\n`);
|
|
1886
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1887
|
+
return 1;
|
|
1888
|
+
}
|
|
1889
|
+
let regions;
|
|
1890
|
+
if (parsedArgs.regionsFilePath !== undefined) {
|
|
1891
|
+
const loaded = loadRegionsFile(parsedArgs.regionsFilePath);
|
|
1892
|
+
if (!loaded.ok) {
|
|
1893
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1894
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1895
|
+
return 1;
|
|
1896
|
+
}
|
|
1897
|
+
// CLI boundary owns file-read/root-wrapper syntax only; region content/geometry validation is owned by isValidReferenceRegions, called inside importExternalReference.
|
|
1898
|
+
regions = loaded.regions;
|
|
1899
|
+
}
|
|
1900
|
+
let requirements;
|
|
1901
|
+
if (parsedArgs.requirementsFilePath !== undefined) {
|
|
1902
|
+
const loaded = loadRequirementsFile(parsedArgs.requirementsFilePath);
|
|
1903
|
+
if (!loaded.ok) {
|
|
1904
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1905
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1906
|
+
return 1;
|
|
1907
|
+
}
|
|
1908
|
+
// CLI boundary owns file-read/root-wrapper syntax only; requirement category/subject/tolerance validation is owned by isValidRawReferenceRequirement/isValidReferenceRequirements, called inside importExternalReference.
|
|
1909
|
+
requirements = loaded.requirements;
|
|
1910
|
+
}
|
|
1911
|
+
let applicability;
|
|
1912
|
+
if (parsedArgs.applicabilityFilePath !== undefined) {
|
|
1913
|
+
const loaded = loadApplicabilityFile(parsedArgs.applicabilityFilePath);
|
|
1914
|
+
if (!loaded.ok) {
|
|
1915
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
1916
|
+
io.stderr(IMPORT_REFERENCE_HELP);
|
|
1917
|
+
return 1;
|
|
1918
|
+
}
|
|
1919
|
+
// CLI boundary owns file-read syntax only; applicability semantics are owned by isValidExternalReferenceApplicability, called inside importExternalReference.
|
|
1920
|
+
applicability = loaded.applicability;
|
|
1921
|
+
}
|
|
1922
|
+
// 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.
|
|
1923
|
+
const result = await importExternalReference(imageBytes, {
|
|
1924
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1925
|
+
...(parsedArgs.label === undefined ? {} : { label: parsedArgs.label }),
|
|
1926
|
+
...(parsedArgs.supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot: parsedArgs.supersedesReferenceRoot }),
|
|
1927
|
+
...(regions === undefined ? {} : { regions }),
|
|
1928
|
+
...(requirements === undefined ? {} : { requirements }),
|
|
1929
|
+
...(applicability === undefined ? {} : { applicability }),
|
|
1930
|
+
});
|
|
1931
|
+
if (!result.ok) {
|
|
1932
|
+
for (const diagnostic of result.diagnostics)
|
|
1933
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1934
|
+
return 1;
|
|
1935
|
+
}
|
|
1936
|
+
io.stdout(`Reference: ${result.referenceId}\n`);
|
|
1937
|
+
io.stdout(`State: imported\n`);
|
|
1938
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1939
|
+
io.stdout(`Image: ${result.imagePath}\n`);
|
|
1940
|
+
io.stdout(`Regions: ${result.regionCount}\n`);
|
|
1941
|
+
io.stdout(`Requirements: ${result.requirementCount}\n`);
|
|
1942
|
+
io.stdout(`Applicability: ${result.hasApplicability ? 'declared' : 'none'}\n`);
|
|
1943
|
+
io.stdout(`Adequacy: ${result.adequacy.status}\n`);
|
|
1944
|
+
return 0;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Thin orchestration only: parse args, then delegate to the existing
|
|
1948
|
+
* `approveExternalReference` application function exactly once. This is the
|
|
1949
|
+
* only command in the observer that approves an external reference.
|
|
1950
|
+
*/
|
|
1951
|
+
async function runApproveReferenceCommand(argv, io) {
|
|
1952
|
+
if (argv.includes('--help')) {
|
|
1953
|
+
io.stdout(APPROVE_REFERENCE_HELP);
|
|
1954
|
+
return 0;
|
|
1955
|
+
}
|
|
1956
|
+
const parsedArgs = parseApproveReferenceArgs(argv);
|
|
1957
|
+
if (!parsedArgs.ok) {
|
|
1958
|
+
for (const error of parsedArgs.errors)
|
|
1959
|
+
io.stderr(`error: ${error}\n`);
|
|
1960
|
+
io.stderr(APPROVE_REFERENCE_HELP);
|
|
1961
|
+
return 1;
|
|
1962
|
+
}
|
|
1963
|
+
// Exactly one application approval attempt: one reference read, an optional supersession-target read, persisted at most once.
|
|
1964
|
+
const result = await approveExternalReference(parsedArgs.referenceRoot, {
|
|
1965
|
+
outputLocation: parsedArgs.outputLocation,
|
|
1966
|
+
...(parsedArgs.supersedesReferenceRoot === undefined ? {} : { supersedesReferenceRoot: parsedArgs.supersedesReferenceRoot }),
|
|
1967
|
+
});
|
|
1968
|
+
if (!result.ok) {
|
|
1969
|
+
for (const diagnostic of result.diagnostics)
|
|
1970
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
1971
|
+
return 1;
|
|
1972
|
+
}
|
|
1973
|
+
io.stdout(`Reference: ${result.referenceId}\n`);
|
|
1974
|
+
io.stdout(`State: approved\n`);
|
|
1975
|
+
io.stdout(`Artifact: ${result.artifactRoot}\n`);
|
|
1976
|
+
io.stdout(`Regions: ${result.regionCount}\n`);
|
|
1977
|
+
io.stdout(`Requirements: ${result.requirementCount}\n`);
|
|
1978
|
+
io.stdout(`Adequacy: ${result.adequacy.status}\n`);
|
|
1979
|
+
io.stdout(`Applicability: ${result.hasApplicability ? 'declared' : 'none'}\n`);
|
|
1980
|
+
return 0;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Thin orchestration only: parse args, load the optional bindings file
|
|
1984
|
+
* (syntax/root-shape only - every binding-declaration rule stays owned by
|
|
1985
|
+
* `isValidReferenceRuntimeBindingDeclarations`, called inside the domain
|
|
1986
|
+
* evaluator), then delegate to the existing
|
|
1987
|
+
* `evaluateReferenceCandidateFidelityFromArtifactRoots` application function
|
|
1988
|
+
* exactly once. No adequacy/compatibility/binding/tolerance/coordinate-
|
|
1989
|
+
* mapping/relationship logic lives here - see
|
|
1990
|
+
* `src/domain/externalReferenceFidelity.ts`. `--enforce` is applied only
|
|
1991
|
+
* after the evaluation has already been computed: it selects the process
|
|
1992
|
+
* exit status for an already-final "fail" fidelity state and never affects
|
|
1993
|
+
* the evaluation's content. Persists nothing.
|
|
1994
|
+
*/
|
|
1995
|
+
async function runEvaluateReferenceFidelityCommand(argv, io) {
|
|
1996
|
+
if (argv.includes('--help')) {
|
|
1997
|
+
io.stdout(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
1998
|
+
return 0;
|
|
1999
|
+
}
|
|
2000
|
+
const parsedArgs = parseEvaluateReferenceFidelityArgs(argv);
|
|
2001
|
+
if (!parsedArgs.ok) {
|
|
2002
|
+
for (const error of parsedArgs.errors)
|
|
2003
|
+
io.stderr(`error: ${error}\n`);
|
|
2004
|
+
io.stderr(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
2005
|
+
return 1;
|
|
2006
|
+
}
|
|
2007
|
+
let bindings = [];
|
|
2008
|
+
if (parsedArgs.bindingsFilePath !== undefined) {
|
|
2009
|
+
const loaded = loadBindingsFile(parsedArgs.bindingsFilePath);
|
|
2010
|
+
if (!loaded.ok) {
|
|
2011
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
2012
|
+
io.stderr(EVALUATE_REFERENCE_FIDELITY_HELP);
|
|
2013
|
+
return 1;
|
|
2014
|
+
}
|
|
2015
|
+
// CLI boundary owns file-read/root-wrapper syntax only; binding-declaration shape/bounds/existence/conflict validation is owned by isValidReferenceRuntimeBindingDeclarations, called inside evaluateReferenceCandidateFidelity.
|
|
2016
|
+
bindings = loaded.bindings;
|
|
2017
|
+
}
|
|
2018
|
+
// Exactly one application evaluation attempt: reads reference/candidate once each, calls the canonical evaluator exactly once.
|
|
2019
|
+
const result = await evaluateReferenceCandidateFidelityFromArtifactRoots(parsedArgs.referenceRoot, parsedArgs.candidateRoot, bindings);
|
|
2020
|
+
if (!result.ok) {
|
|
2021
|
+
for (const diagnostic of result.diagnostics)
|
|
2022
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
2023
|
+
return 1;
|
|
2024
|
+
}
|
|
2025
|
+
const { evaluation } = result;
|
|
2026
|
+
const passCount = evaluation.requirementResults.filter((r) => r.status === 'pass').length;
|
|
2027
|
+
const failCount = evaluation.requirementResults.filter((r) => r.status === 'fail').length;
|
|
2028
|
+
const unavailableCount = evaluation.requirementResults.filter((r) => r.status === 'unavailable').length;
|
|
2029
|
+
io.stdout(`Reference: ${evaluation.referenceId}\n`);
|
|
2030
|
+
io.stdout(`Candidate: ${evaluation.candidateObservationId}\n`);
|
|
2031
|
+
io.stdout(`Adequacy: ${evaluation.adequacy.status}\n`);
|
|
2032
|
+
if (evaluation.compatibility !== undefined)
|
|
2033
|
+
io.stdout(`Compatibility: ${evaluation.compatibility.state}\n`);
|
|
2034
|
+
io.stdout(`State: ${evaluation.state}\n`);
|
|
2035
|
+
if (evaluation.blockedBy !== undefined)
|
|
2036
|
+
io.stdout(`Blocked by: ${evaluation.blockedBy}\n`);
|
|
2037
|
+
io.stdout(`Requirements: ${evaluation.requirementResults.length} (pass: ${passCount}, fail: ${failCount}, unavailable: ${unavailableCount})\n`);
|
|
2038
|
+
io.stdout(`Enforced: ${parsedArgs.enforce ? 'yes' : 'no'}\n`);
|
|
2039
|
+
if (parsedArgs.enforce && evaluation.state === 'fail')
|
|
2040
|
+
return 1;
|
|
2041
|
+
return 0;
|
|
2042
|
+
}
|
|
2043
|
+
async function runInitCommand(argv, io) {
|
|
2044
|
+
if (argv.includes('--help')) {
|
|
2045
|
+
io.stdout(INIT_HELP);
|
|
2046
|
+
return 0;
|
|
2047
|
+
}
|
|
2048
|
+
const parsed = parseInitArgs(argv);
|
|
2049
|
+
if (!parsed.ok) {
|
|
2050
|
+
for (const error of parsed.errors)
|
|
2051
|
+
io.stderr(`error: ${error}\n`);
|
|
2052
|
+
io.stderr(INIT_HELP);
|
|
2053
|
+
return 1;
|
|
2054
|
+
}
|
|
2055
|
+
let targets = parsed.targets;
|
|
2056
|
+
if (parsed.targetsFilePath !== undefined) {
|
|
2057
|
+
const loaded = loadTargetsFile(parsed.targetsFilePath);
|
|
2058
|
+
if (!loaded.ok) {
|
|
2059
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
2060
|
+
return 1;
|
|
2061
|
+
}
|
|
2062
|
+
targets = loaded.targets;
|
|
2063
|
+
}
|
|
2064
|
+
const result = await initializeFrontendObserverProject({ projectRoot: process.cwd(), url: parsed.url, viewport: parsed.viewport, targets: targets, ...(parsed.defaultBaseline === undefined ? {} : { defaultBaseline: parsed.defaultBaseline }), replace: parsed.replace });
|
|
2065
|
+
if (!result.ok) {
|
|
2066
|
+
io.stderr(`error [${result.code}]: ${result.message}\n`);
|
|
2067
|
+
return 1;
|
|
2068
|
+
}
|
|
2069
|
+
io.stdout(`Initialized frontend Observer project: ${result.configPath}\n`);
|
|
2070
|
+
return 0;
|
|
2071
|
+
}
|
|
2072
|
+
async function runCaptureCommand(argv, io) {
|
|
2073
|
+
if (argv.includes('--help')) {
|
|
2074
|
+
io.stdout(CAPTURE_HELP);
|
|
2075
|
+
return 0;
|
|
2076
|
+
}
|
|
2077
|
+
const positional = argv.filter((arg) => !arg.startsWith('--'));
|
|
2078
|
+
const unknown = argv.filter((arg) => arg.startsWith('--') && arg !== '--replace');
|
|
2079
|
+
if (positional.length !== 1 || unknown.length > 0) {
|
|
2080
|
+
io.stderr('error: capture requires exactly one alias and supports only --replace\n');
|
|
2081
|
+
io.stderr(CAPTURE_HELP);
|
|
2082
|
+
return 1;
|
|
2083
|
+
}
|
|
2084
|
+
const discovered = await discoverFrontendObserverProject(process.cwd());
|
|
2085
|
+
if (!discovered.ok) {
|
|
2086
|
+
io.stderr('error [project-not-initialized]: no frontend-observer.json was found in this directory or its parents; run "my-frontend-observer init"\n');
|
|
2087
|
+
return 1;
|
|
2088
|
+
}
|
|
2089
|
+
const result = await captureNamedObservation({ projectRoot: discovered.projectRoot, alias: positional[0], replace: argv.includes('--replace') });
|
|
2090
|
+
if (!result.ok) {
|
|
2091
|
+
if (result.diagnostics !== undefined)
|
|
2092
|
+
for (const diagnostic of result.diagnostics)
|
|
2093
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
2094
|
+
io.stderr(`error [${result.code}]: ${result.message}\n`);
|
|
2095
|
+
return 1;
|
|
2096
|
+
}
|
|
2097
|
+
io.stdout(`Alias: ${result.alias}\nObservation: ${result.observationId}\nRequest: ${result.requestId}\nCompletion: ${result.completionState}\nArtifact: ${result.artifactRoot}\n`);
|
|
2098
|
+
for (const diagnostic of result.diagnostics)
|
|
2099
|
+
io.stdout(`${formatDiagnostic(diagnostic)}\n`);
|
|
2100
|
+
return NON_SUCCESS_COMPLETION_STATES.has(result.completionState) ? 1 : 0;
|
|
2101
|
+
}
|
|
2102
|
+
export function formatCheckHumanResult(result) {
|
|
2103
|
+
const lines = [`Check: ${result.status}`];
|
|
2104
|
+
if (result.baseline !== undefined)
|
|
2105
|
+
lines.push(`Baseline: ${result.baseline.alias}`);
|
|
2106
|
+
if (result.candidate !== undefined)
|
|
2107
|
+
lines.push(`Candidate: ${result.candidate.alias}`);
|
|
2108
|
+
if (result.comparison.differenceCount !== undefined) {
|
|
2109
|
+
const suffix = result.comparison.differenceCount === 1 ? 'difference' : 'differences';
|
|
2110
|
+
lines.push(`Comparison: ${result.comparison.state}, ${result.comparison.differenceCount} ${suffix}`);
|
|
2111
|
+
}
|
|
2112
|
+
else if (result.comparison.state !== 'NOT_RUN')
|
|
2113
|
+
lines.push(`Comparison: ${result.comparison.state}`);
|
|
2114
|
+
lines.push(`Contract: ${result.contract.configured ? result.contract.state : 'not configured'}`);
|
|
2115
|
+
for (const clause of result.contract.failedClauses)
|
|
2116
|
+
lines.push(` ${clause.category ?? clause.source} ${clause.clauseId}: ${clause.status.toUpperCase()}`);
|
|
2117
|
+
lines.push(`Reference: ${result.reference.configured ? result.reference.state : 'not configured'}`);
|
|
2118
|
+
for (const requirement of result.reference.failedRequirements)
|
|
2119
|
+
lines.push(` ${requirement.category} ${requirement.requirementId}: FAIL`);
|
|
2120
|
+
lines.push(`Unexpected changes: ${result.contract.unexpectedChanges.length}`);
|
|
2121
|
+
for (const blocker of result.blockers)
|
|
2122
|
+
lines.push(`Blocker: ${blocker.code} - ${blocker.message}`);
|
|
2123
|
+
if (result.status === 'REVIEW_REQUIRED')
|
|
2124
|
+
lines.push('Acceptance: no executable contract or approved reference is configured.');
|
|
2125
|
+
lines.push('Inspect: my-frontend-observer view');
|
|
2126
|
+
return `${lines.join('\n')}\n`;
|
|
2127
|
+
}
|
|
2128
|
+
async function runCheckCommand(argv, io) {
|
|
2129
|
+
if (argv.includes('--help')) {
|
|
2130
|
+
io.stdout(CHECK_HELP);
|
|
2131
|
+
return 0;
|
|
2132
|
+
}
|
|
2133
|
+
const unknown = argv.filter((arg) => arg.startsWith('--') && arg !== '--json');
|
|
2134
|
+
const positional = argv.filter((arg) => !arg.startsWith('--'));
|
|
2135
|
+
if (unknown.length > 0 || positional.length > 1 || argv.filter((arg) => arg === '--json').length > 1) {
|
|
2136
|
+
io.stderr('error: check accepts at most one baseline alias and one --json flag\n');
|
|
2137
|
+
io.stderr(CHECK_HELP);
|
|
2138
|
+
return 1;
|
|
2139
|
+
}
|
|
2140
|
+
const discovered = await discoverFrontendObserverProject(process.cwd());
|
|
2141
|
+
let result;
|
|
2142
|
+
if (!discovered.ok) {
|
|
2143
|
+
result = (await import('./projectWorkflow/checkResult.js')).emptyCheckResult();
|
|
2144
|
+
result.blockers.push({ code: 'project-not-initialized', message: 'check requires an initialized project' });
|
|
2145
|
+
}
|
|
2146
|
+
else
|
|
2147
|
+
result = await checkProject(discovered.projectRoot, positional[0]);
|
|
2148
|
+
io.stdout(argv.includes('--json') ? `${JSON.stringify(result)}\n` : formatCheckHumanResult(result));
|
|
2149
|
+
return { PASS: 0, FAIL: 1, REVIEW_REQUIRED: 2, BLOCKED: 3 }[result.status];
|
|
2150
|
+
}
|
|
2151
|
+
/** CLI-syntax-only parsing, mirroring `parseApproveBaselineArgs`. `--port` shape/range checking happens here; root existence/directory-ness is the application layer's job (see `startViewer`). */
|
|
2152
|
+
function parseViewArgs(argv) {
|
|
2153
|
+
const errors = [];
|
|
2154
|
+
let root;
|
|
2155
|
+
let rootFlagCount = 0;
|
|
2156
|
+
let port;
|
|
2157
|
+
let portFlagCount = 0;
|
|
2158
|
+
let noOpen = false;
|
|
2159
|
+
let bindingsFilePath;
|
|
2160
|
+
let bindingsFileFlagCount = 0;
|
|
2161
|
+
let contextFilePath;
|
|
2162
|
+
let contextFileFlagCount = 0;
|
|
2163
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
2164
|
+
const arg = argv[i];
|
|
2165
|
+
switch (arg) {
|
|
2166
|
+
case '--root': {
|
|
2167
|
+
const value = argv[(i += 1)];
|
|
2168
|
+
rootFlagCount += 1;
|
|
2169
|
+
if (value === undefined)
|
|
2170
|
+
errors.push('--root requires a path argument');
|
|
2171
|
+
else if (rootFlagCount > 1)
|
|
2172
|
+
errors.push('--root may only be specified once');
|
|
2173
|
+
else
|
|
2174
|
+
root = value;
|
|
2175
|
+
break;
|
|
2176
|
+
}
|
|
2177
|
+
case '--port': {
|
|
2178
|
+
const value = argv[(i += 1)];
|
|
2179
|
+
portFlagCount += 1;
|
|
2180
|
+
if (value === undefined) {
|
|
2181
|
+
errors.push('--port requires a numeric argument');
|
|
2182
|
+
}
|
|
2183
|
+
else if (portFlagCount > 1) {
|
|
2184
|
+
errors.push('--port may only be specified once');
|
|
2185
|
+
}
|
|
2186
|
+
else {
|
|
2187
|
+
const parsed = Number(value);
|
|
2188
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
2189
|
+
errors.push(`--port must be an integer between 0 and 65535; got ${JSON.stringify(value)}`);
|
|
2190
|
+
}
|
|
2191
|
+
else {
|
|
2192
|
+
port = parsed;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
case '--no-open':
|
|
2198
|
+
noOpen = true;
|
|
2199
|
+
break;
|
|
2200
|
+
case '--bindings-file': {
|
|
2201
|
+
const value = argv[(i += 1)];
|
|
2202
|
+
bindingsFileFlagCount += 1;
|
|
2203
|
+
if (value === undefined)
|
|
2204
|
+
errors.push('--bindings-file requires a file path argument');
|
|
2205
|
+
else if (bindingsFileFlagCount > 1)
|
|
2206
|
+
errors.push('--bindings-file may only be specified once');
|
|
2207
|
+
else
|
|
2208
|
+
bindingsFilePath = value;
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
case '--context-file': {
|
|
2212
|
+
const value = argv[(i += 1)];
|
|
2213
|
+
contextFileFlagCount += 1;
|
|
2214
|
+
if (value === undefined)
|
|
2215
|
+
errors.push('--context-file requires a file path argument');
|
|
2216
|
+
else if (contextFileFlagCount > 1)
|
|
2217
|
+
errors.push('--context-file may only be specified once');
|
|
2218
|
+
else
|
|
2219
|
+
contextFilePath = value;
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
default:
|
|
2223
|
+
errors.push(`unrecognized argument: ${arg}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (errors.length > 0)
|
|
2227
|
+
return { ok: false, errors };
|
|
2228
|
+
return {
|
|
2229
|
+
ok: true,
|
|
2230
|
+
...(root === undefined ? {} : { root }),
|
|
2231
|
+
...(port === undefined ? {} : { port }),
|
|
2232
|
+
noOpen,
|
|
2233
|
+
...(bindingsFilePath === undefined ? {} : { bindingsFilePath }),
|
|
2234
|
+
...(contextFilePath === undefined ? {} : { contextFilePath }),
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Thin orchestration only: parse args, delegate to the existing
|
|
2239
|
+
* `startViewer` application function exactly once, print status, and
|
|
2240
|
+
* optionally attempt a best-effort browser open. Never parses Observer
|
|
2241
|
+
* artifacts, never derives evidence, never mutates anything. Returns as soon
|
|
2242
|
+
* as the server is confirmed listening (or has failed to start) - the
|
|
2243
|
+
* process itself keeps running afterward only because the server's open
|
|
2244
|
+
* listening socket keeps the Node event loop alive, not because this
|
|
2245
|
+
* function blocks.
|
|
2246
|
+
*/
|
|
2247
|
+
async function runViewCommand(argv, io) {
|
|
2248
|
+
if (argv.includes('--help')) {
|
|
2249
|
+
io.stdout(VIEW_HELP);
|
|
2250
|
+
return 0;
|
|
2251
|
+
}
|
|
2252
|
+
const parsedArgs = parseViewArgs(argv);
|
|
2253
|
+
if (!parsedArgs.ok) {
|
|
2254
|
+
for (const error of parsedArgs.errors)
|
|
2255
|
+
io.stderr(`error: ${error}\n`);
|
|
2256
|
+
io.stderr(VIEW_HELP);
|
|
2257
|
+
return 1;
|
|
2258
|
+
}
|
|
2259
|
+
// Reuses the exact same operational binding-file wrapper parser as `evaluate-reference-fidelity --bindings-file`
|
|
2260
|
+
// (see loadBindingsFile above) - one shared parser, never a second divergent one. Reference-specific declaration
|
|
2261
|
+
// validity (region existence, shape) is deferred to the moment a reference is actually selected in the viewer,
|
|
2262
|
+
// via the existing canonical isValidReferenceRuntimeBindingDeclarations - never checked here without a reference.
|
|
2263
|
+
let bindingDeclarations = [];
|
|
2264
|
+
if (parsedArgs.bindingsFilePath !== undefined) {
|
|
2265
|
+
const loaded = loadBindingsFile(parsedArgs.bindingsFilePath);
|
|
2266
|
+
if (!loaded.ok) {
|
|
2267
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
2268
|
+
io.stderr(VIEW_HELP);
|
|
2269
|
+
return 1;
|
|
2270
|
+
}
|
|
2271
|
+
if (!Array.isArray(loaded.bindings)) {
|
|
2272
|
+
io.stderr('error: --bindings-file "bindings" property must be an array\n');
|
|
2273
|
+
io.stderr(VIEW_HELP);
|
|
2274
|
+
return 1;
|
|
2275
|
+
}
|
|
2276
|
+
bindingDeclarations = loaded.bindings;
|
|
2277
|
+
}
|
|
2278
|
+
// Reuses the exact same canonical validator (isValidBoundedAgentContextArtifact, via
|
|
2279
|
+
// classifyContextFileContent) that owns current-schema structural validity - never a second validator.
|
|
2280
|
+
// A recognized-kind, non-current-schema file is not a startup failure (task §16); every other problem is.
|
|
2281
|
+
let contextState = { status: 'none' };
|
|
2282
|
+
if (parsedArgs.contextFilePath !== undefined) {
|
|
2283
|
+
const loaded = loadContextFile(parsedArgs.contextFilePath);
|
|
2284
|
+
if (!loaded.ok) {
|
|
2285
|
+
io.stderr(`error: ${loaded.error}\n`);
|
|
2286
|
+
io.stderr(VIEW_HELP);
|
|
2287
|
+
return 1;
|
|
2288
|
+
}
|
|
2289
|
+
contextState = loaded.state;
|
|
2290
|
+
}
|
|
2291
|
+
let root;
|
|
2292
|
+
let aliasMetadata;
|
|
2293
|
+
if (parsedArgs.root !== undefined) {
|
|
2294
|
+
root = parsedArgs.root;
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
const discovered = await discoverFrontendObserverProject(process.cwd());
|
|
2298
|
+
if (!discovered.ok) {
|
|
2299
|
+
io.stderr('error [project-not-initialized]: view without --root requires an initialized project\n');
|
|
2300
|
+
return 1;
|
|
2301
|
+
}
|
|
2302
|
+
const projectState = await loadProjectViewerState(discovered.projectRoot);
|
|
2303
|
+
if (!projectState.ok) {
|
|
2304
|
+
io.stderr(`error [${projectState.code}]: ${projectState.message}\n`);
|
|
2305
|
+
return 1;
|
|
2306
|
+
}
|
|
2307
|
+
root = projectState.root;
|
|
2308
|
+
aliasMetadata = { observationAliasesByRelativeDir: projectState.aliases };
|
|
2309
|
+
}
|
|
2310
|
+
const result = await startViewer({
|
|
2311
|
+
root,
|
|
2312
|
+
...(parsedArgs.port === undefined ? {} : { port: parsedArgs.port }),
|
|
2313
|
+
bindingDeclarations,
|
|
2314
|
+
context: contextState,
|
|
2315
|
+
...(aliasMetadata === undefined ? {} : { aliasMetadata }),
|
|
2316
|
+
});
|
|
2317
|
+
if (!result.ok) {
|
|
2318
|
+
for (const diagnostic of result.diagnostics)
|
|
2319
|
+
io.stderr(`${formatDiagnostic(diagnostic)}\n`);
|
|
2320
|
+
return 1;
|
|
2321
|
+
}
|
|
2322
|
+
io.stdout(`Viewer: ${result.url}\n`);
|
|
2323
|
+
io.stdout(`Root: ${result.root}\n`);
|
|
2324
|
+
io.stdout(`Press Ctrl+C to stop.\n`);
|
|
2325
|
+
if (!parsedArgs.noOpen) {
|
|
2326
|
+
try {
|
|
2327
|
+
await openInDefaultBrowser(result.url);
|
|
2328
|
+
}
|
|
2329
|
+
catch (err) {
|
|
2330
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2331
|
+
io.stderr(`note: could not open the default browser automatically: ${message}\n`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
return 0;
|
|
2335
|
+
}
|
|
2336
|
+
/** Testable CLI entry point: pure function of argv (+ injectable IO), no direct process.exit. */
|
|
2337
|
+
export async function runCli(argv, io = defaultIO) {
|
|
2338
|
+
const [command, ...rest] = argv;
|
|
2339
|
+
if (command === undefined) {
|
|
2340
|
+
io.stderr(TOP_LEVEL_HELP);
|
|
2341
|
+
return 1;
|
|
2342
|
+
}
|
|
2343
|
+
if (command === '--help' || command === '-h') {
|
|
2344
|
+
io.stdout(TOP_LEVEL_HELP);
|
|
2345
|
+
return 0;
|
|
2346
|
+
}
|
|
2347
|
+
if (command === '--version') {
|
|
2348
|
+
io.stdout(`${getProducerInfo().version}\n`);
|
|
2349
|
+
return 0;
|
|
2350
|
+
}
|
|
2351
|
+
if (command === 'observe') {
|
|
2352
|
+
return runObserveCommand(rest, io);
|
|
2353
|
+
}
|
|
2354
|
+
if (command === 'init')
|
|
2355
|
+
return runInitCommand(rest, io);
|
|
2356
|
+
if (command === 'capture')
|
|
2357
|
+
return runCaptureCommand(rest, io);
|
|
2358
|
+
if (command === 'check')
|
|
2359
|
+
return runCheckCommand(rest, io);
|
|
2360
|
+
if (command === 'compare') {
|
|
2361
|
+
return runCompareCommand(rest, io);
|
|
2362
|
+
}
|
|
2363
|
+
if (command === 'approve-baseline') {
|
|
2364
|
+
return runApproveBaselineCommand(rest, io);
|
|
2365
|
+
}
|
|
2366
|
+
if (command === 'save-change-contract') {
|
|
2367
|
+
return runSaveChangeContractCommand(rest, io);
|
|
2368
|
+
}
|
|
2369
|
+
if (command === 'evaluate-contract') {
|
|
2370
|
+
return runEvaluateContractCommand(rest, io);
|
|
2371
|
+
}
|
|
2372
|
+
if (command === 'import-reference') {
|
|
2373
|
+
return runImportReferenceCommand(rest, io);
|
|
2374
|
+
}
|
|
2375
|
+
if (command === 'approve-reference') {
|
|
2376
|
+
return runApproveReferenceCommand(rest, io);
|
|
2377
|
+
}
|
|
2378
|
+
if (command === 'evaluate-reference-fidelity') {
|
|
2379
|
+
return runEvaluateReferenceFidelityCommand(rest, io);
|
|
2380
|
+
}
|
|
2381
|
+
if (command === 'view') {
|
|
2382
|
+
return runViewCommand(rest, io);
|
|
2383
|
+
}
|
|
2384
|
+
io.stderr(`error: unrecognized command "${command}"\n`);
|
|
2385
|
+
io.stderr(TOP_LEVEL_HELP);
|
|
2386
|
+
return 1;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Resolves symlinks on both sides before comparing paths, not just a raw URL string
|
|
2390
|
+
* comparison: on macOS, `os.tmpdir()` (and other paths) live under `/var`, which is
|
|
2391
|
+
* itself a symlink to `/private/var`. A plain `import.meta.url === pathToFileURL(...)`
|
|
2392
|
+
* comparison silently evaluates false in that case (mismatched but equivalent paths),
|
|
2393
|
+
* so the CLI's own entry point never runs - `node dist/cli.js ...` exits 0 having
|
|
2394
|
+
* printed nothing. Real-pathing both sides makes the comparison symlink-safe.
|
|
2395
|
+
*/
|
|
2396
|
+
function isMainModule() {
|
|
2397
|
+
if (process.argv[1] === undefined)
|
|
2398
|
+
return false;
|
|
2399
|
+
try {
|
|
2400
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
2401
|
+
}
|
|
2402
|
+
catch {
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
if (isMainModule()) {
|
|
2407
|
+
runCli(process.argv.slice(2)).then((exitCode) => {
|
|
2408
|
+
process.exitCode = exitCode;
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
//# sourceMappingURL=cli.js.map
|