@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.
Files changed (316) hide show
  1. package/CHANGELOG.md +397 -0
  2. package/LICENSE +21 -0
  3. package/README.md +255 -0
  4. package/dist/application/browserCaptureService.d.ts +11 -0
  5. package/dist/application/browserCaptureService.js +12 -0
  6. package/dist/application/browserCaptureService.js.map +1 -0
  7. package/dist/application/comparisonService.d.ts +56 -0
  8. package/dist/application/comparisonService.js +77 -0
  9. package/dist/application/comparisonService.js.map +1 -0
  10. package/dist/application/externalReferencePersistenceService.d.ts +75 -0
  11. package/dist/application/externalReferencePersistenceService.js +182 -0
  12. package/dist/application/externalReferencePersistenceService.js.map +1 -0
  13. package/dist/application/frontendContractEvaluationService.d.ts +49 -0
  14. package/dist/application/frontendContractEvaluationService.js +112 -0
  15. package/dist/application/frontendContractEvaluationService.js.map +1 -0
  16. package/dist/application/frontendContractPersistenceService.d.ts +56 -0
  17. package/dist/application/frontendContractPersistenceService.js +91 -0
  18. package/dist/application/frontendContractPersistenceService.js.map +1 -0
  19. package/dist/application/observationPersistence.d.ts +59 -0
  20. package/dist/application/observationPersistence.js +79 -0
  21. package/dist/application/observationPersistence.js.map +1 -0
  22. package/dist/application/projectCheckService.d.ts +3 -0
  23. package/dist/application/projectCheckService.js +169 -0
  24. package/dist/application/projectCheckService.js.map +1 -0
  25. package/dist/application/projectWorkflowService.d.ts +46 -0
  26. package/dist/application/projectWorkflowService.js +92 -0
  27. package/dist/application/projectWorkflowService.js.map +1 -0
  28. package/dist/application/referenceFidelityEvaluationService.d.ts +28 -0
  29. package/dist/application/referenceFidelityEvaluationService.js +45 -0
  30. package/dist/application/referenceFidelityEvaluationService.js.map +1 -0
  31. package/dist/artifacts/artifactReader.d.ts +19 -0
  32. package/dist/artifacts/artifactReader.js +36 -0
  33. package/dist/artifacts/artifactReader.js.map +1 -0
  34. package/dist/artifacts/artifactWriter.d.ts +25 -0
  35. package/dist/artifacts/artifactWriter.js +68 -0
  36. package/dist/artifacts/artifactWriter.js.map +1 -0
  37. package/dist/artifacts/comparisonArtifactReader.d.ts +18 -0
  38. package/dist/artifacts/comparisonArtifactReader.js +35 -0
  39. package/dist/artifacts/comparisonArtifactReader.js.map +1 -0
  40. package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
  41. package/dist/artifacts/comparisonArtifactWriter.js +67 -0
  42. package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
  43. package/dist/artifacts/externalReferenceArtifactReader.d.ts +18 -0
  44. package/dist/artifacts/externalReferenceArtifactReader.js +35 -0
  45. package/dist/artifacts/externalReferenceArtifactReader.js.map +1 -0
  46. package/dist/artifacts/externalReferenceArtifactWriter.d.ts +44 -0
  47. package/dist/artifacts/externalReferenceArtifactWriter.js +77 -0
  48. package/dist/artifacts/externalReferenceArtifactWriter.js.map +1 -0
  49. package/dist/artifacts/frontendContractArtifactReader.d.ts +24 -0
  50. package/dist/artifacts/frontendContractArtifactReader.js +47 -0
  51. package/dist/artifacts/frontendContractArtifactReader.js.map +1 -0
  52. package/dist/artifacts/frontendContractArtifactWriter.d.ts +34 -0
  53. package/dist/artifacts/frontendContractArtifactWriter.js +70 -0
  54. package/dist/artifacts/frontendContractArtifactWriter.js.map +1 -0
  55. package/dist/artifacts/frontendContractEvaluationArtifactReader.d.ts +17 -0
  56. package/dist/artifacts/frontendContractEvaluationArtifactReader.js +34 -0
  57. package/dist/artifacts/frontendContractEvaluationArtifactReader.js.map +1 -0
  58. package/dist/artifacts/frontendContractEvaluationArtifactWriter.d.ts +32 -0
  59. package/dist/artifacts/frontendContractEvaluationArtifactWriter.js +58 -0
  60. package/dist/artifacts/frontendContractEvaluationArtifactWriter.js.map +1 -0
  61. package/dist/artifacts/types.d.ts +17 -0
  62. package/dist/artifacts/types.js +2 -0
  63. package/dist/artifacts/types.js.map +1 -0
  64. package/dist/browser/chromiumAdapter.d.ts +21 -0
  65. package/dist/browser/chromiumAdapter.js +204 -0
  66. package/dist/browser/chromiumAdapter.js.map +1 -0
  67. package/dist/browser/evidenceCapture.d.ts +62 -0
  68. package/dist/browser/evidenceCapture.js +500 -0
  69. package/dist/browser/evidenceCapture.js.map +1 -0
  70. package/dist/browser/scrollCapture.d.ts +32 -0
  71. package/dist/browser/scrollCapture.js +163 -0
  72. package/dist/browser/scrollCapture.js.map +1 -0
  73. package/dist/browser/types.d.ts +24 -0
  74. package/dist/browser/types.js +2 -0
  75. package/dist/browser/types.js.map +1 -0
  76. package/dist/cli.d.ts +8 -0
  77. package/dist/cli.js +2411 -0
  78. package/dist/cli.js.map +1 -0
  79. package/dist/domain/boundedAgentContext.d.ts +226 -0
  80. package/dist/domain/boundedAgentContext.js +355 -0
  81. package/dist/domain/boundedAgentContext.js.map +1 -0
  82. package/dist/domain/boundedAgentContextCorrelation.d.ts +74 -0
  83. package/dist/domain/boundedAgentContextCorrelation.js +441 -0
  84. package/dist/domain/boundedAgentContextCorrelation.js.map +1 -0
  85. package/dist/domain/boundedAgentContextIdentity.d.ts +26 -0
  86. package/dist/domain/boundedAgentContextIdentity.js +69 -0
  87. package/dist/domain/boundedAgentContextIdentity.js.map +1 -0
  88. package/dist/domain/boundedAgentContextProjection.d.ts +71 -0
  89. package/dist/domain/boundedAgentContextProjection.js +477 -0
  90. package/dist/domain/boundedAgentContextProjection.js.map +1 -0
  91. package/dist/domain/comparison.d.ts +220 -0
  92. package/dist/domain/comparison.js +350 -0
  93. package/dist/domain/comparison.js.map +1 -0
  94. package/dist/domain/comparisonEngine.d.ts +76 -0
  95. package/dist/domain/comparisonEngine.js +734 -0
  96. package/dist/domain/comparisonEngine.js.map +1 -0
  97. package/dist/domain/comparisonIdentity.d.ts +13 -0
  98. package/dist/domain/comparisonIdentity.js +46 -0
  99. package/dist/domain/comparisonIdentity.js.map +1 -0
  100. package/dist/domain/completion.d.ts +30 -0
  101. package/dist/domain/completion.js +22 -0
  102. package/dist/domain/completion.js.map +1 -0
  103. package/dist/domain/diagnostics.d.ts +17 -0
  104. package/dist/domain/diagnostics.js +77 -0
  105. package/dist/domain/diagnostics.js.map +1 -0
  106. package/dist/domain/evidence.d.ts +29 -0
  107. package/dist/domain/evidence.js +59 -0
  108. package/dist/domain/evidence.js.map +1 -0
  109. package/dist/domain/explicitState.d.ts +40 -0
  110. package/dist/domain/explicitState.js +54 -0
  111. package/dist/domain/explicitState.js.map +1 -0
  112. package/dist/domain/externalReference.d.ts +158 -0
  113. package/dist/domain/externalReference.js +167 -0
  114. package/dist/domain/externalReference.js.map +1 -0
  115. package/dist/domain/externalReferenceApplicability.d.ts +43 -0
  116. package/dist/domain/externalReferenceApplicability.js +52 -0
  117. package/dist/domain/externalReferenceApplicability.js.map +1 -0
  118. package/dist/domain/externalReferenceCompatibility.d.ts +40 -0
  119. package/dist/domain/externalReferenceCompatibility.js +48 -0
  120. package/dist/domain/externalReferenceCompatibility.js.map +1 -0
  121. package/dist/domain/externalReferenceFidelity.d.ts +171 -0
  122. package/dist/domain/externalReferenceFidelity.js +419 -0
  123. package/dist/domain/externalReferenceFidelity.js.map +1 -0
  124. package/dist/domain/externalReferenceIdentity.d.ts +38 -0
  125. package/dist/domain/externalReferenceIdentity.js +70 -0
  126. package/dist/domain/externalReferenceIdentity.js.map +1 -0
  127. package/dist/domain/externalReferenceImage.d.ts +35 -0
  128. package/dist/domain/externalReferenceImage.js +160 -0
  129. package/dist/domain/externalReferenceImage.js.map +1 -0
  130. package/dist/domain/externalReferenceRegionRelationships.d.ts +63 -0
  131. package/dist/domain/externalReferenceRegionRelationships.js +98 -0
  132. package/dist/domain/externalReferenceRegionRelationships.js.map +1 -0
  133. package/dist/domain/externalReferenceRegions.d.ts +65 -0
  134. package/dist/domain/externalReferenceRegions.js +105 -0
  135. package/dist/domain/externalReferenceRegions.js.map +1 -0
  136. package/dist/domain/externalReferenceRequirementIdentity.d.ts +12 -0
  137. package/dist/domain/externalReferenceRequirementIdentity.js +35 -0
  138. package/dist/domain/externalReferenceRequirementIdentity.js.map +1 -0
  139. package/dist/domain/externalReferenceRequirements.d.ts +215 -0
  140. package/dist/domain/externalReferenceRequirements.js +401 -0
  141. package/dist/domain/externalReferenceRequirements.js.map +1 -0
  142. package/dist/domain/externalReferenceRuntimeBinding.d.ts +146 -0
  143. package/dist/domain/externalReferenceRuntimeBinding.js +183 -0
  144. package/dist/domain/externalReferenceRuntimeBinding.js.map +1 -0
  145. package/dist/domain/frontendContractEvaluation.d.ts +57 -0
  146. package/dist/domain/frontendContractEvaluation.js +454 -0
  147. package/dist/domain/frontendContractEvaluation.js.map +1 -0
  148. package/dist/domain/frontendContractEvaluationArtifact.d.ts +65 -0
  149. package/dist/domain/frontendContractEvaluationArtifact.js +108 -0
  150. package/dist/domain/frontendContractEvaluationArtifact.js.map +1 -0
  151. package/dist/domain/frontendContractIdentity.d.ts +39 -0
  152. package/dist/domain/frontendContractIdentity.js +70 -0
  153. package/dist/domain/frontendContractIdentity.js.map +1 -0
  154. package/dist/domain/frontendContracts.d.ts +188 -0
  155. package/dist/domain/frontendContracts.js +260 -0
  156. package/dist/domain/frontendContracts.js.map +1 -0
  157. package/dist/domain/identity.d.ts +21 -0
  158. package/dist/domain/identity.js +47 -0
  159. package/dist/domain/identity.js.map +1 -0
  160. package/dist/domain/referenceCorrectionIdentity.d.ts +40 -0
  161. package/dist/domain/referenceCorrectionIdentity.js +77 -0
  162. package/dist/domain/referenceCorrectionIdentity.js.map +1 -0
  163. package/dist/domain/referenceCorrectionWorkflow.d.ts +160 -0
  164. package/dist/domain/referenceCorrectionWorkflow.js +165 -0
  165. package/dist/domain/referenceCorrectionWorkflow.js.map +1 -0
  166. package/dist/domain/referenceFidelityProjection.d.ts +65 -0
  167. package/dist/domain/referenceFidelityProjection.js +135 -0
  168. package/dist/domain/referenceFidelityProjection.js.map +1 -0
  169. package/dist/domain/relationships.d.ts +210 -0
  170. package/dist/domain/relationships.js +352 -0
  171. package/dist/domain/relationships.js.map +1 -0
  172. package/dist/domain/schema.d.ts +269 -0
  173. package/dist/domain/schema.js +442 -0
  174. package/dist/domain/schema.js.map +1 -0
  175. package/dist/domain/scrollEvidence.d.ts +51 -0
  176. package/dist/domain/scrollEvidence.js +134 -0
  177. package/dist/domain/scrollEvidence.js.map +1 -0
  178. package/dist/index.d.ts +114 -0
  179. package/dist/index.js +64 -0
  180. package/dist/index.js.map +1 -0
  181. package/dist/projectWorkflow/aliasCatalog.d.ts +21 -0
  182. package/dist/projectWorkflow/aliasCatalog.js +67 -0
  183. package/dist/projectWorkflow/aliasCatalog.js.map +1 -0
  184. package/dist/projectWorkflow/checkAcceptance.d.ts +25 -0
  185. package/dist/projectWorkflow/checkAcceptance.js +55 -0
  186. package/dist/projectWorkflow/checkAcceptance.js.map +1 -0
  187. package/dist/projectWorkflow/checkResult.d.ts +85 -0
  188. package/dist/projectWorkflow/checkResult.js +101 -0
  189. package/dist/projectWorkflow/checkResult.js.map +1 -0
  190. package/dist/projectWorkflow/projectConfig.d.ts +43 -0
  191. package/dist/projectWorkflow/projectConfig.js +84 -0
  192. package/dist/projectWorkflow/projectConfig.js.map +1 -0
  193. package/dist/projectWorkflow/projectDiscovery.d.ts +9 -0
  194. package/dist/projectWorkflow/projectDiscovery.js +20 -0
  195. package/dist/projectWorkflow/projectDiscovery.js.map +1 -0
  196. package/dist/projectWorkflow/projectPaths.d.ts +8 -0
  197. package/dist/projectWorkflow/projectPaths.js +23 -0
  198. package/dist/projectWorkflow/projectPaths.js.map +1 -0
  199. package/dist/request/paths.d.ts +14 -0
  200. package/dist/request/paths.js +33 -0
  201. package/dist/request/paths.js.map +1 -0
  202. package/dist/request/request.d.ts +111 -0
  203. package/dist/request/request.js +464 -0
  204. package/dist/request/request.js.map +1 -0
  205. package/dist/safety/policy.d.ts +14 -0
  206. package/dist/safety/policy.js +81 -0
  207. package/dist/safety/policy.js.map +1 -0
  208. package/dist/viewer/assets/index-CN_yb9Uf.css +1 -0
  209. package/dist/viewer/assets/index-D98S1_2d.js +9 -0
  210. package/dist/viewer/icons/icon-192.png +0 -0
  211. package/dist/viewer/icons/icon-512.png +0 -0
  212. package/dist/viewer/index.html +15 -0
  213. package/dist/viewer/manifest.webmanifest +1 -0
  214. package/dist/viewer/registerSW.js +1 -0
  215. package/dist/viewer/sw.js +1 -0
  216. package/dist/viewer/workbox-9c191d2f.js +1 -0
  217. package/dist/viewerServer/context.d.ts +48 -0
  218. package/dist/viewerServer/context.js +60 -0
  219. package/dist/viewerServer/context.js.map +1 -0
  220. package/dist/viewerServer/evidence/classify.d.ts +59 -0
  221. package/dist/viewerServer/evidence/classify.js +124 -0
  222. package/dist/viewerServer/evidence/classify.js.map +1 -0
  223. package/dist/viewerServer/evidence/comparisonView.d.ts +28 -0
  224. package/dist/viewerServer/evidence/comparisonView.js +43 -0
  225. package/dist/viewerServer/evidence/comparisonView.js.map +1 -0
  226. package/dist/viewerServer/evidence/contextSourceView.d.ts +25 -0
  227. package/dist/viewerServer/evidence/contextSourceView.js +20 -0
  228. package/dist/viewerServer/evidence/contextSourceView.js.map +1 -0
  229. package/dist/viewerServer/evidence/discovery.d.ts +31 -0
  230. package/dist/viewerServer/evidence/discovery.js +78 -0
  231. package/dist/viewerServer/evidence/discovery.js.map +1 -0
  232. package/dist/viewerServer/evidence/evaluationView.d.ts +32 -0
  233. package/dist/viewerServer/evidence/evaluationView.js +50 -0
  234. package/dist/viewerServer/evidence/evaluationView.js.map +1 -0
  235. package/dist/viewerServer/evidence/handles.d.ts +21 -0
  236. package/dist/viewerServer/evidence/handles.js +43 -0
  237. package/dist/viewerServer/evidence/handles.js.map +1 -0
  238. package/dist/viewerServer/evidence/index.d.ts +41 -0
  239. package/dist/viewerServer/evidence/index.js +82 -0
  240. package/dist/viewerServer/evidence/index.js.map +1 -0
  241. package/dist/viewerServer/evidence/limits.d.ts +27 -0
  242. package/dist/viewerServer/evidence/limits.js +28 -0
  243. package/dist/viewerServer/evidence/limits.js.map +1 -0
  244. package/dist/viewerServer/evidence/linkedEvidence.d.ts +43 -0
  245. package/dist/viewerServer/evidence/linkedEvidence.js +151 -0
  246. package/dist/viewerServer/evidence/linkedEvidence.js.map +1 -0
  247. package/dist/viewerServer/evidence/mediaResolver.d.ts +16 -0
  248. package/dist/viewerServer/evidence/mediaResolver.js +85 -0
  249. package/dist/viewerServer/evidence/mediaResolver.js.map +1 -0
  250. package/dist/viewerServer/evidence/observationView.d.ts +29 -0
  251. package/dist/viewerServer/evidence/observationView.js +46 -0
  252. package/dist/viewerServer/evidence/observationView.js.map +1 -0
  253. package/dist/viewerServer/evidence/pathSafety.d.ts +11 -0
  254. package/dist/viewerServer/evidence/pathSafety.js +31 -0
  255. package/dist/viewerServer/evidence/pathSafety.js.map +1 -0
  256. package/dist/viewerServer/evidence/projection.d.ts +55 -0
  257. package/dist/viewerServer/evidence/projection.js +152 -0
  258. package/dist/viewerServer/evidence/projection.js.map +1 -0
  259. package/dist/viewerServer/evidence/referenceView.d.ts +133 -0
  260. package/dist/viewerServer/evidence/referenceView.js +169 -0
  261. package/dist/viewerServer/evidence/referenceView.js.map +1 -0
  262. package/dist/viewerServer/httpServer.d.ts +27 -0
  263. package/dist/viewerServer/httpServer.js +381 -0
  264. package/dist/viewerServer/httpServer.js.map +1 -0
  265. package/dist/viewerServer/openBrowser.d.ts +7 -0
  266. package/dist/viewerServer/openBrowser.js +32 -0
  267. package/dist/viewerServer/openBrowser.js.map +1 -0
  268. package/dist/viewerServer/port.d.ts +16 -0
  269. package/dist/viewerServer/port.js +19 -0
  270. package/dist/viewerServer/port.js.map +1 -0
  271. package/dist/viewerServer/viewerService.d.ts +62 -0
  272. package/dist/viewerServer/viewerService.js +88 -0
  273. package/dist/viewerServer/viewerService.js.map +1 -0
  274. package/docs/ARCHITECTURE.md +1286 -0
  275. package/docs/CI_CD.md +250 -0
  276. package/docs/COMMANDS.md +972 -0
  277. package/docs/CONTRACTS.md +1856 -0
  278. package/docs/CURRENT_STATE.md +1049 -0
  279. package/docs/DEVELOPMENT.md +202 -0
  280. package/docs/DOCUMENTATION_PRESERVATION_POLICY.md +50 -0
  281. package/docs/PROJECT_DESCRIPTION.md +2221 -0
  282. package/docs/PROJECT_MILESTONES.md +2526 -0
  283. package/docs/PROJECT_OVERVIEW.md +150 -0
  284. package/docs/QUICKSTART.md +83 -0
  285. package/docs/RELEASE.md +27 -0
  286. package/docs/ROADMAP.md +641 -0
  287. package/docs/SECURITY.md +218 -0
  288. package/docs/WORKFLOWS.md +642 -0
  289. package/docs/plans/v0.8-implementation-plan.md +655 -0
  290. package/docs/plans/v0.8.1-cli-usability-patch-plan.md +505 -0
  291. package/docs/reports/v0.7-bounded-fidelity-context-prompt7.md +243 -0
  292. package/docs/reports/v0.7-implementation-completeness-documentation-reconciliation.md +497 -0
  293. package/docs/reports/v0.7-pre-release-readiness.md +337 -0
  294. package/docs/reports/v0.7-reference-binding-prompt5.md +223 -0
  295. package/docs/reports/v0.7-reference-compatibility-prompt4.md +234 -0
  296. package/docs/reports/v0.7-reference-correction-workflow-prompt8.md +222 -0
  297. package/docs/reports/v0.7-reference-fidelity-prompt6.md +216 -0
  298. package/docs/reports/v0.7-reference-foundation-prompt1.md +151 -0
  299. package/docs/reports/v0.7-reference-regions-prompt2.md +195 -0
  300. package/docs/reports/v0.7-reference-requirements-prompt3.md +217 -0
  301. package/docs/reports/v0.7-release-prep.md +423 -0
  302. package/docs/reports/v0.8-binding-fidelity-interaction-batch6.md +279 -0
  303. package/docs/reports/v0.8-bounded-context-correlation-batch7.md +233 -0
  304. package/docs/reports/v0.8-comparison-contract-inspection-batch4.md +279 -0
  305. package/docs/reports/v0.8-evidence-index-readers-batch2.md +247 -0
  306. package/docs/reports/v0.8-implementation-completeness-documentation-reconciliation.md +741 -0
  307. package/docs/reports/v0.8-integrated-viewer-acceptance-batch8.md +128 -0
  308. package/docs/reports/v0.8-observation-svg-inspection-batch3.md +223 -0
  309. package/docs/reports/v0.8-prerelease-readiness-cross-platform-security-code-rot.md +687 -0
  310. package/docs/reports/v0.8-reference-candidate-inspection-batch5.md +232 -0
  311. package/docs/reports/v0.8-viewer-runtime-pwa-batch1.md +278 -0
  312. package/docs/reports/v0.8.1-check-orchestration-prompt2.md +69 -0
  313. package/docs/reports/v0.8.1-implementation-completeness-documentation-reconciliation.md +114 -0
  314. package/docs/reports/v0.8.1-prerelease-readiness-cross-platform-security-code-rot.md +170 -0
  315. package/docs/reports/v0.8.1-project-workflow-foundation-prompt1.md +66 -0
  316. package/package.json +58 -0
@@ -0,0 +1,734 @@
1
+ import { evidenceValue } from './evidence.js';
2
+ import { isValidObservationArtifact, getProducerInfo } from './schema.js';
3
+ import { deriveOverflowEvidence } from './scrollEvidence.js';
4
+ import { deriveLayoutRelationships, deriveTargetClipping, isValidGeometryTolerancePx, GEOMETRY_TOLERANCE_DEFAULT_PX } from './relationships.js';
5
+ import { COMPARISON_ARTIFACT_KIND, COMPARISON_SCHEMA_VERSION, COMPARABILITY_REASON_SEVERITY, COMPARABILITY_REASON_CODES, isValidComparisonConfig, isValidComparisonArtifact, } from './comparison.js';
6
+ import { buildComparisonRequestIdentity, buildComparisonIdentity } from './comparisonIdentity.js';
7
+ import { DIAGNOSTIC_SEVERITY } from './diagnostics.js';
8
+ // --- small local helpers -----------------------------------------------------
9
+ /** `evidenceValue` for a field that may itself be absent from a dictionary lookup (e.g. `pageEvidence['someKey']`, an optional `containment` cast) - never fabricates a value for a missing field. */
10
+ function fieldValue(field) {
11
+ return field ? evidenceValue(field) : undefined;
12
+ }
13
+ function normalizedTargetMap(targets) {
14
+ const map = new Map();
15
+ for (const target of targets)
16
+ map.set(target.name.toLowerCase(), target);
17
+ return map;
18
+ }
19
+ /** Deterministic order: before's configured order, restricted to names also configured after (see domain/comparisonEngine.ts#compareTargetConfiguration for the matching addition/removal ordering). */
20
+ function commonTargets(before, after) {
21
+ const afterMap = normalizedTargetMap(after.requestConfig.targets);
22
+ const results = [];
23
+ for (const target of before.requestConfig.targets) {
24
+ const afterTarget = afterMap.get(target.name.toLowerCase());
25
+ if (afterTarget)
26
+ results.push({ canonicalName: target.name, beforeName: target.name, afterName: afterTarget.name });
27
+ }
28
+ return results;
29
+ }
30
+ /**
31
+ * Configuration-change evidence: `removed`/`locator-changed` in before's
32
+ * configured order, then `added` in after's configured order (Batch 1's
33
+ * frozen ordering guidance) - never a source of appeared/disappeared
34
+ * rendered-state differences by itself.
35
+ */
36
+ export function compareTargetConfiguration(before, after) {
37
+ const changes = [];
38
+ const beforeMap = normalizedTargetMap(before.requestConfig.targets);
39
+ const afterMap = normalizedTargetMap(after.requestConfig.targets);
40
+ for (const target of before.requestConfig.targets) {
41
+ const key = target.name.toLowerCase();
42
+ const afterTarget = afterMap.get(key);
43
+ if (!afterTarget) {
44
+ changes.push({ kind: 'removed', target: target.name });
45
+ }
46
+ else if (JSON.stringify(target.locators) !== JSON.stringify(afterTarget.locators)) {
47
+ changes.push({ kind: 'locator-changed', target: target.name });
48
+ }
49
+ }
50
+ for (const target of after.requestConfig.targets) {
51
+ if (!beforeMap.has(target.name.toLowerCase()))
52
+ changes.push({ kind: 'added', target: target.name });
53
+ }
54
+ return changes;
55
+ }
56
+ function isSameScrollScenario(before, after) {
57
+ if (before === undefined && after === undefined)
58
+ return true;
59
+ if (before === undefined || after === undefined)
60
+ return false;
61
+ const a = before.action;
62
+ const b = after.action;
63
+ if (a.kind !== b.kind)
64
+ return false;
65
+ if (a.deltaX !== b.deltaX || a.deltaY !== b.deltaY)
66
+ return false;
67
+ if (a.kind === 'target-scroll-by' && b.kind === 'target-scroll-by') {
68
+ return a.target.toLowerCase() === b.target.toLowerCase();
69
+ }
70
+ return true;
71
+ }
72
+ // --- comparability -----------------------------------------------------------
73
+ /**
74
+ * v0.7 Prompt 4 shared pure dimension-comparison rule, used by both
75
+ * `evaluateComparability` below (v0.4, ObservationArtifact <-> ObservationArtifact -
76
+ * both sides always technically present, but `explicitState` sub-fields are
77
+ * each independently optional) and
78
+ * `domain/externalReferenceCompatibility.ts#evaluateReferenceCandidateCompatibility`
79
+ * (v0.7, where the reference-side value is optional by construction - a
80
+ * reference need not declare an applicable viewport/theme/state at all).
81
+ * One rule, three outcomes, matching request section 16 G/H/I exactly:
82
+ * both sides present and equal -> no reason at all (the existing "matching
83
+ * dimensions produce no reason" convention); both present and different ->
84
+ * the blocking mismatch reason; either side absent -> the unassessed
85
+ * reason (never silently treated as compatible, never a fabricated
86
+ * mismatch merely because one side omitted the dimension).
87
+ */
88
+ export function assessOptionalComparabilityDimension(mismatchCode, unassessedCode, beforeValue, afterValue, mismatchMessage, unassessedMessage) {
89
+ if (beforeValue !== undefined && afterValue !== undefined) {
90
+ if (beforeValue === afterValue)
91
+ return undefined;
92
+ return {
93
+ code: mismatchCode,
94
+ severity: COMPARABILITY_REASON_SEVERITY[mismatchCode],
95
+ message: mismatchMessage(beforeValue, afterValue),
96
+ referenceValue: beforeValue,
97
+ candidateValue: afterValue,
98
+ };
99
+ }
100
+ return { code: unassessedCode, severity: COMPARABILITY_REASON_SEVERITY[unassessedCode], message: unassessedMessage };
101
+ }
102
+ /**
103
+ * Evaluates comparability before any rendered difference is calculated
104
+ * (Batch 1's frozen `ComparabilityResult`/reason vocabulary). Hard
105
+ * incompatibilities (`blocking`) force `incomparable`; producer/browser
106
+ * version and target-configuration mismatches are `warning`-only. The three
107
+ * state dimensions (theme, authenticated-state, application-state) are
108
+ * assessed - matching or blocking-mismatched - whenever both observations
109
+ * declare that dimension via v0.7 Prompt 4's `requestConfig.explicitState`;
110
+ * otherwise (the entire pre-Prompt-4 case, and any Prompt-4-era observation
111
+ * that simply omits a dimension) they remain `unassessed`, exactly as
112
+ * before - never silently claimed identical.
113
+ */
114
+ export function evaluateComparability(before, after) {
115
+ const reasons = [];
116
+ const add = (code, message) => {
117
+ reasons.push({ code, severity: COMPARABILITY_REASON_SEVERITY[code], message });
118
+ };
119
+ if (before.artifactKind !== after.artifactKind) {
120
+ add('artifact-kind-mismatch', `before artifactKind "${before.artifactKind}" differs from after artifactKind "${after.artifactKind}"`);
121
+ }
122
+ if (before.schemaVersion !== after.schemaVersion) {
123
+ add('schema-version-mismatch', `before schemaVersion "${before.schemaVersion}" differs from after schemaVersion "${after.schemaVersion}"`);
124
+ }
125
+ if (before.requestConfig.targetUrl !== after.requestConfig.targetUrl) {
126
+ add('page-url-mismatch', `before targetUrl "${before.requestConfig.targetUrl}" differs from after targetUrl "${after.requestConfig.targetUrl}"`);
127
+ }
128
+ if (before.requestConfig.viewport.width !== after.requestConfig.viewport.width || before.requestConfig.viewport.height !== after.requestConfig.viewport.height) {
129
+ add('viewport-mismatch', `before viewport ${before.requestConfig.viewport.width}x${before.requestConfig.viewport.height} differs from after viewport ${after.requestConfig.viewport.width}x${after.requestConfig.viewport.height}`);
130
+ }
131
+ const beforeBrowser = evidenceValue(before.browser);
132
+ const afterBrowser = evidenceValue(after.browser);
133
+ if (beforeBrowser && afterBrowser && beforeBrowser.engine !== afterBrowser.engine) {
134
+ add('browser-engine-mismatch', `before browser engine "${beforeBrowser.engine}" differs from after browser engine "${afterBrowser.engine}"`);
135
+ }
136
+ if (beforeBrowser && afterBrowser && beforeBrowser.version !== afterBrowser.version) {
137
+ add('browser-version-mismatch', `before browser version "${beforeBrowser.version}" differs from after browser version "${afterBrowser.version}"`);
138
+ }
139
+ if (!isSameScrollScenario(before.requestConfig.scrollScenario, after.requestConfig.scrollScenario)) {
140
+ add('scroll-scenario-mismatch', 'before and after requestConfig.scrollScenario are not the same semantic scroll scenario');
141
+ }
142
+ if (before.producer.version !== after.producer.version) {
143
+ add('producer-version-mismatch', `before producer version "${before.producer.version}" differs from after producer version "${after.producer.version}"`);
144
+ }
145
+ const configurationChanges = compareTargetConfiguration(before, after);
146
+ if (configurationChanges.some((change) => change.kind === 'added' || change.kind === 'removed')) {
147
+ add('target-set-mismatch', 'configured target sets differ between before and after');
148
+ }
149
+ if (configurationChanges.some((change) => change.kind === 'locator-changed')) {
150
+ add('target-locator-mismatch', 'a stable target name has a changed locator configuration between before and after');
151
+ }
152
+ const beforeState = before.requestConfig.explicitState;
153
+ const afterState = after.requestConfig.explicitState;
154
+ const themeReason = assessOptionalComparabilityDimension('theme-mismatch', 'theme-unassessed', beforeState?.theme, afterState?.theme, (b, a) => `before explicit theme "${b}" differs from after explicit theme "${a}"`, 'theme identity is not modeled by the observer unless both observations declare requestConfig.explicitState.theme');
155
+ if (themeReason)
156
+ reasons.push(themeReason);
157
+ const authReason = assessOptionalComparabilityDimension('authenticated-state-mismatch', 'authenticated-state-unassessed', beforeState?.authenticatedState, afterState?.authenticatedState, (b, a) => `before explicit authenticatedState "${b}" differs from after explicit authenticatedState "${a}"`, 'authenticated-state identity is not modeled by the observer unless both observations declare requestConfig.explicitState.authenticatedState');
158
+ if (authReason)
159
+ reasons.push(authReason);
160
+ const appStateReason = assessOptionalComparabilityDimension('application-state-mismatch', 'application-state-unassessed', beforeState?.applicationState, afterState?.applicationState, (b, a) => `before explicit applicationState "${b}" differs from after explicit applicationState "${a}"`, 'application-state identity is not modeled by the observer unless both observations declare requestConfig.explicitState.applicationState');
161
+ if (appStateReason)
162
+ reasons.push(appStateReason);
163
+ reasons.sort((a, b) => COMPARABILITY_REASON_CODES.indexOf(a.code) - COMPARABILITY_REASON_CODES.indexOf(b.code));
164
+ const hasBlocking = reasons.some((reason) => reason.severity === 'blocking');
165
+ const hasWarning = reasons.some((reason) => reason.severity === 'warning');
166
+ const state = hasBlocking ? 'incomparable' : hasWarning ? 'comparable-with-warnings' : 'comparable';
167
+ return { state, reasons };
168
+ }
169
+ export function targetPresence(record) {
170
+ if (!record)
171
+ return 'unavailable';
172
+ const resolution = evidenceValue(record.resolution);
173
+ if (!resolution)
174
+ return 'unavailable';
175
+ if (resolution.selectionStatus === 'matched')
176
+ return 'matched';
177
+ if (resolution.selectionStatus === 'not-found')
178
+ return 'not-found';
179
+ if (resolution.selectionStatus === 'ambiguous')
180
+ return 'ambiguous';
181
+ return 'unavailable';
182
+ }
183
+ function geometryEvidenceRef(name) {
184
+ return [{ path: `targetEvidence.${name}.geometry` }];
185
+ }
186
+ function targetOverflow(record) {
187
+ const layout = evidenceValue(record.layout);
188
+ const style = evidenceValue(record.style);
189
+ if (!layout || !style)
190
+ return undefined;
191
+ return deriveOverflowEvidence(layout, style.overflowX, style.overflowY);
192
+ }
193
+ /**
194
+ * Direct per-target rendered differences (moved/resized/visibility/clipping/
195
+ * actual-overflow) plus appeared/disappeared, for one common (same stable
196
+ * name both sides) target. `diagnostics` records an honest inability to
197
+ * assess rendered state (ambiguous/unavailable on either side) rather than
198
+ * fabricating absence - see Batch 1's frozen "ambiguous/unavailable is not
199
+ * definitely absent" rule.
200
+ */
201
+ function compareOneTarget(before, after, target, tolerance, diagnostics) {
202
+ const differences = [];
203
+ const beforeRecord = before.targetEvidence[target.beforeName];
204
+ const afterRecord = after.targetEvidence[target.afterName];
205
+ const beforeStatus = targetPresence(beforeRecord);
206
+ const afterStatus = targetPresence(afterRecord);
207
+ const subject = { type: 'target', target: target.canonicalName };
208
+ if (beforeStatus === 'not-found' && afterStatus === 'matched') {
209
+ differences.push({
210
+ kind: 'appeared',
211
+ subject,
212
+ before: 'not-found',
213
+ after: 'matched',
214
+ classification: 'presence',
215
+ beforeObservationId: before.observationId,
216
+ afterObservationId: after.observationId,
217
+ evidence: geometryEvidenceRef(target.afterName),
218
+ });
219
+ return differences;
220
+ }
221
+ if (beforeStatus === 'matched' && afterStatus === 'not-found') {
222
+ differences.push({
223
+ kind: 'disappeared',
224
+ subject,
225
+ before: 'matched',
226
+ after: 'not-found',
227
+ classification: 'presence',
228
+ beforeObservationId: before.observationId,
229
+ afterObservationId: after.observationId,
230
+ evidence: geometryEvidenceRef(target.beforeName),
231
+ });
232
+ return differences;
233
+ }
234
+ if (beforeStatus === 'not-found' && afterStatus === 'not-found') {
235
+ return differences;
236
+ }
237
+ if (beforeStatus !== 'matched' || afterStatus !== 'matched') {
238
+ const code = beforeStatus === 'ambiguous' || afterStatus === 'ambiguous' ? 'target-ambiguous' : 'browser-evidence-unavailable';
239
+ diagnostics.push({
240
+ code,
241
+ severity: DIAGNOSTIC_SEVERITY[code],
242
+ message: `target "${target.canonicalName}" rendered-state comparison unavailable (before: ${beforeStatus}, after: ${afterStatus})`,
243
+ targetName: target.canonicalName,
244
+ });
245
+ return differences;
246
+ }
247
+ // Both sides matched: compare rendered state directly.
248
+ const beforeGeometry = beforeRecord ? evidenceValue(beforeRecord.geometry) : undefined;
249
+ const afterGeometry = afterRecord ? evidenceValue(afterRecord.geometry) : undefined;
250
+ if (beforeGeometry && afterGeometry) {
251
+ const dx = afterGeometry.x - beforeGeometry.x;
252
+ const dy = afterGeometry.y - beforeGeometry.y;
253
+ if (Math.abs(dx) > tolerance || Math.abs(dy) > tolerance) {
254
+ differences.push({
255
+ kind: 'moved',
256
+ subject,
257
+ before: { x: beforeGeometry.x, y: beforeGeometry.y },
258
+ after: { x: afterGeometry.x, y: afterGeometry.y },
259
+ delta: { x: dx, y: dy },
260
+ classification: 'position',
261
+ beforeObservationId: before.observationId,
262
+ afterObservationId: after.observationId,
263
+ evidence: [...geometryEvidenceRef(target.beforeName), ...geometryEvidenceRef(target.afterName)],
264
+ });
265
+ }
266
+ const dw = afterGeometry.width - beforeGeometry.width;
267
+ const dh = afterGeometry.height - beforeGeometry.height;
268
+ if (Math.abs(dw) > tolerance || Math.abs(dh) > tolerance) {
269
+ differences.push({
270
+ kind: 'resized',
271
+ subject,
272
+ before: { width: beforeGeometry.width, height: beforeGeometry.height },
273
+ after: { width: afterGeometry.width, height: afterGeometry.height },
274
+ delta: { width: dw, height: dh },
275
+ classification: 'size',
276
+ beforeObservationId: before.observationId,
277
+ afterObservationId: after.observationId,
278
+ evidence: [...geometryEvidenceRef(target.beforeName), ...geometryEvidenceRef(target.afterName)],
279
+ });
280
+ }
281
+ }
282
+ const beforeVisible = beforeRecord ? evidenceValue(beforeRecord.visibility)?.visible : undefined;
283
+ const afterVisible = afterRecord ? evidenceValue(afterRecord.visibility)?.visible : undefined;
284
+ if (beforeVisible !== undefined && afterVisible !== undefined && beforeVisible !== afterVisible) {
285
+ differences.push({
286
+ kind: 'visibility-changed',
287
+ subject,
288
+ before: beforeVisible,
289
+ after: afterVisible,
290
+ classification: 'visibility',
291
+ beforeObservationId: before.observationId,
292
+ afterObservationId: after.observationId,
293
+ evidence: [{ path: `targetEvidence.${target.beforeName}.visibility` }, { path: `targetEvidence.${target.afterName}.visibility` }],
294
+ });
295
+ }
296
+ if (beforeRecord && afterRecord) {
297
+ const beforeClip = deriveTargetClipping(beforeRecord);
298
+ const afterClip = deriveTargetClipping(afterRecord);
299
+ const clipEvidence = [{ path: `targetEvidence.${target.beforeName}.layout` }, { path: `targetEvidence.${target.afterName}.layout` }];
300
+ if (beforeClip.horizontal !== 'unavailable' && afterClip.horizontal !== 'unavailable' && beforeClip.horizontal !== afterClip.horizontal) {
301
+ differences.push({
302
+ kind: 'clipping-changed',
303
+ subject,
304
+ before: beforeClip.horizontal,
305
+ after: afterClip.horizontal,
306
+ classification: 'horizontal',
307
+ beforeObservationId: before.observationId,
308
+ afterObservationId: after.observationId,
309
+ evidence: clipEvidence,
310
+ });
311
+ }
312
+ if (beforeClip.vertical !== 'unavailable' && afterClip.vertical !== 'unavailable' && beforeClip.vertical !== afterClip.vertical) {
313
+ differences.push({
314
+ kind: 'clipping-changed',
315
+ subject,
316
+ before: beforeClip.vertical,
317
+ after: afterClip.vertical,
318
+ classification: 'vertical',
319
+ beforeObservationId: before.observationId,
320
+ afterObservationId: after.observationId,
321
+ evidence: clipEvidence,
322
+ });
323
+ }
324
+ const beforeOverflow = targetOverflow(beforeRecord);
325
+ const afterOverflow = targetOverflow(afterRecord);
326
+ if (beforeOverflow && afterOverflow) {
327
+ if (beforeOverflow.horizontalOverflow !== afterOverflow.horizontalOverflow) {
328
+ differences.push({
329
+ kind: 'horizontal-overflow-changed',
330
+ subject,
331
+ before: beforeOverflow.horizontalOverflow,
332
+ after: afterOverflow.horizontalOverflow,
333
+ classification: 'horizontal',
334
+ beforeObservationId: before.observationId,
335
+ afterObservationId: after.observationId,
336
+ evidence: clipEvidence,
337
+ });
338
+ }
339
+ if (beforeOverflow.verticalOverflow !== afterOverflow.verticalOverflow) {
340
+ differences.push({
341
+ kind: 'vertical-overflow-changed',
342
+ subject,
343
+ before: beforeOverflow.verticalOverflow,
344
+ after: afterOverflow.verticalOverflow,
345
+ classification: 'vertical',
346
+ beforeObservationId: before.observationId,
347
+ afterObservationId: after.observationId,
348
+ evidence: clipEvidence,
349
+ });
350
+ }
351
+ }
352
+ }
353
+ return differences;
354
+ }
355
+ /** Pairwise DOM-containment comparison among matched-both-sides common targets - reuses existing `TargetContainment` evidence directly, never re-derived from geometry (Batch 2's frozen distinction). Only evaluated when the specific pair was actually evaluated on both sides. */
356
+ function compareContainment(before, after, matched) {
357
+ const differences = [];
358
+ for (let i = 0; i < matched.length; i += 1) {
359
+ for (let j = 0; j < matched.length; j += 1) {
360
+ if (i === j)
361
+ continue;
362
+ const subjectTarget = matched[i];
363
+ const otherTarget = matched[j];
364
+ const beforeContainment = fieldValue(before.targetEvidence[subjectTarget.beforeName]?.containment);
365
+ const afterContainment = fieldValue(after.targetEvidence[subjectTarget.afterName]?.containment);
366
+ if (!beforeContainment || !afterContainment)
367
+ continue;
368
+ const beforeEvaluated = beforeContainment.evaluatedTargetIds.includes(otherTarget.beforeName);
369
+ const afterEvaluated = afterContainment.evaluatedTargetIds.includes(otherTarget.afterName);
370
+ if (!beforeEvaluated || !afterEvaluated)
371
+ continue;
372
+ const beforeContained = beforeContainment.containedByTargetIds.includes(otherTarget.beforeName);
373
+ const afterContained = afterContainment.containedByTargetIds.includes(otherTarget.afterName);
374
+ if (beforeContained === afterContained)
375
+ continue;
376
+ differences.push({
377
+ kind: 'containment-changed',
378
+ subject: { type: 'target', target: subjectTarget.canonicalName },
379
+ before: beforeContained,
380
+ after: afterContained,
381
+ classification: `contained-by:${otherTarget.canonicalName}`,
382
+ beforeObservationId: before.observationId,
383
+ afterObservationId: after.observationId,
384
+ evidence: [
385
+ { path: `targetEvidence.${subjectTarget.beforeName}.containment` },
386
+ { path: `targetEvidence.${subjectTarget.afterName}.containment` },
387
+ ],
388
+ });
389
+ }
390
+ }
391
+ return differences;
392
+ }
393
+ function comparePageSize(before, after, tolerance) {
394
+ const beforeWidth = fieldValue(before.pageEvidence.documentWidth);
395
+ const afterWidth = fieldValue(after.pageEvidence.documentWidth);
396
+ const beforeHeight = fieldValue(before.pageEvidence.documentHeight);
397
+ const afterHeight = fieldValue(after.pageEvidence.documentHeight);
398
+ if (typeof beforeWidth !== 'number' || typeof afterWidth !== 'number' || typeof beforeHeight !== 'number' || typeof afterHeight !== 'number') {
399
+ return [];
400
+ }
401
+ const dw = afterWidth - beforeWidth;
402
+ const dh = afterHeight - beforeHeight;
403
+ if (Math.abs(dw) <= tolerance && Math.abs(dh) <= tolerance)
404
+ return [];
405
+ return [
406
+ {
407
+ kind: 'page-size-changed',
408
+ subject: { type: 'page' },
409
+ before: { width: beforeWidth, height: beforeHeight },
410
+ after: { width: afterWidth, height: afterHeight },
411
+ delta: { width: dw, height: dh },
412
+ classification: 'document-size',
413
+ beforeObservationId: before.observationId,
414
+ afterObservationId: after.observationId,
415
+ evidence: [{ path: 'pageEvidence.documentWidth' }, { path: 'pageEvidence.documentHeight' }],
416
+ },
417
+ ];
418
+ }
419
+ function scrollOwnerEqual(a, b) {
420
+ if (a.kind !== b.kind)
421
+ return false;
422
+ if (a.kind === 'target' && b.kind === 'target')
423
+ return a.target === b.target;
424
+ return true;
425
+ }
426
+ /** Only meaningful when both sides have actually-derived scroll-owner evidence (comparability already ensures the scenario *configuration* matches; this compares the *runtime result*). Never fabricated when either side's evidence is unavailable. */
427
+ function compareScrollOwner(before, after) {
428
+ const beforeOwner = before.scrollScenarioEvidence ? evidenceValue(before.scrollScenarioEvidence.scrollOwner) : undefined;
429
+ const afterOwner = after.scrollScenarioEvidence ? evidenceValue(after.scrollScenarioEvidence.scrollOwner) : undefined;
430
+ if (!beforeOwner || !afterOwner || scrollOwnerEqual(beforeOwner, afterOwner))
431
+ return [];
432
+ return [
433
+ {
434
+ kind: 'scroll-owner-changed',
435
+ subject: { type: 'page' },
436
+ before: beforeOwner,
437
+ after: afterOwner,
438
+ classification: 'scroll-owner',
439
+ beforeObservationId: before.observationId,
440
+ afterObservationId: after.observationId,
441
+ evidence: [{ path: 'scrollScenarioEvidence.scrollOwner' }],
442
+ },
443
+ ];
444
+ }
445
+ // --- relationship-change derivation -------------------------------------------
446
+ const RELATIVE_POSITION_KINDS = [
447
+ 'left-of',
448
+ 'right-of',
449
+ 'horizontally-overlapping',
450
+ 'above',
451
+ 'below',
452
+ 'vertically-overlapping',
453
+ 'overlaps',
454
+ 'does-not-overlap',
455
+ ];
456
+ function isRelativePositionFamily(kind) {
457
+ return RELATIVE_POSITION_KINDS.includes(kind);
458
+ }
459
+ /**
460
+ * A pair carries up to one record *per family* (see
461
+ * `deriveLayoutRelationships`), so looking up "the" record for
462
+ * (subjectTarget, relatedTarget) is ambiguous without also naming the
463
+ * family - matching by subject/related alone would silently return
464
+ * whichever family's record happens to appear first in the array.
465
+ */
466
+ function findPairwiseInFamily(graph, subjectTarget, relatedTarget, family) {
467
+ return graph.pairwiseRelationships.find((r) => r.subjectTarget === subjectTarget && r.relatedTarget === relatedTarget && family.includes(r.kind));
468
+ }
469
+ const RELATIONSHIP_FAMILY_GROUPS = [
470
+ ['left-of', 'right-of', 'horizontally-overlapping'],
471
+ ['above', 'below', 'vertically-overlapping'],
472
+ ['overlaps', 'does-not-overlap'],
473
+ ['wider-than', 'narrower-than', 'equal-width-within-tolerance'],
474
+ ['fits-inside', 'does-not-fit-inside'],
475
+ ['follows-vertically'],
476
+ ];
477
+ /**
478
+ * Matches relationship records by structural identity (family + subject +
479
+ * related target), never by array position. Direction is taken from
480
+ * `before`'s own configured order for each pair; if `after`'s graph placed
481
+ * the same pair in the reversed direction (or the pair became unresolved),
482
+ * the `after` side of the family is honestly reported `'unavailable'`
483
+ * (the frozen escape hatch) rather than guessed/inverted - relationship
484
+ * direction inversion under target-order changes is a known, documented
485
+ * scope limitation, not a fabricated result.
486
+ */
487
+ function deriveRelationshipChanges(relationshipsBefore, relationshipsAfter, matched) {
488
+ const changes = [];
489
+ for (let i = 0; i < matched.length; i += 1) {
490
+ for (let j = i + 1; j < matched.length; j += 1) {
491
+ const x = matched[i];
492
+ const y = matched[j];
493
+ for (const family of RELATIONSHIP_FAMILY_GROUPS) {
494
+ const beforeKind = findPairwiseInFamily(relationshipsBefore, x.beforeName, y.beforeName, family)?.kind;
495
+ const afterKind = findPairwiseInFamily(relationshipsAfter, x.afterName, y.afterName, family)?.kind;
496
+ if (beforeKind === afterKind)
497
+ continue;
498
+ if (beforeKind === undefined && afterKind === undefined)
499
+ continue;
500
+ changes.push({
501
+ scope: 'pairwise',
502
+ kind: (beforeKind ?? afterKind),
503
+ subjectTarget: x.canonicalName,
504
+ relatedTarget: y.canonicalName,
505
+ before: beforeKind ?? 'unavailable',
506
+ after: afterKind ?? 'unavailable',
507
+ beforeObservationId: relationshipsBefore.observationId,
508
+ afterObservationId: relationshipsAfter.observationId,
509
+ });
510
+ }
511
+ }
512
+ }
513
+ const beforePage = relationshipsBefore.pageRelationships[0];
514
+ const afterPage = relationshipsAfter.pageRelationships[0];
515
+ if (beforePage || afterPage) {
516
+ if (beforePage?.kind !== afterPage?.kind) {
517
+ changes.push({
518
+ scope: 'page',
519
+ kind: (beforePage?.kind ?? afterPage?.kind),
520
+ before: beforePage?.kind ?? 'unavailable',
521
+ after: afterPage?.kind ?? 'unavailable',
522
+ beforeObservationId: relationshipsBefore.observationId,
523
+ afterObservationId: relationshipsAfter.observationId,
524
+ });
525
+ }
526
+ }
527
+ return changes;
528
+ }
529
+ /**
530
+ * One user-facing `ComparisonDifference` per relationship change -
531
+ * `relative-position-changed` for the horizontal-order/vertical-order/
532
+ * area-overlap families, `relationship-changed` for the rest (Batch 1's
533
+ * frozen distinction between absolute target movement and a relative
534
+ * relationship transition). Evidence is built directly from the change's own
535
+ * target/page identity - every family's supporting evidence for a given
536
+ * pair is the same two geometry references (see
537
+ * `deriveLayoutRelationships#geometryEvidence`), so no graph lookup (and no
538
+ * risk of picking a different family's record) is needed here.
539
+ */
540
+ function relationshipChangesToDifferences(changes, beforeObservationId, afterObservationId) {
541
+ return changes.map((change) => {
542
+ const kind = change.scope === 'pairwise' && isRelativePositionFamily(change.kind) ? 'relative-position-changed' : 'relationship-changed';
543
+ const subject = change.scope === 'page' || !change.subjectTarget || !change.relatedTarget
544
+ ? { type: 'relationship', kind: change.kind }
545
+ : { type: 'relationship', kind: change.kind, subjectTarget: change.subjectTarget, relatedTarget: change.relatedTarget };
546
+ const evidence = change.scope === 'pairwise' && change.subjectTarget && change.relatedTarget
547
+ ? [{ path: `targetEvidence.${change.subjectTarget}.geometry` }, { path: `targetEvidence.${change.relatedTarget}.geometry` }]
548
+ : [{ path: 'pageEvidence.documentWidth' }, { path: 'pageEvidence.viewportWidth' }];
549
+ return {
550
+ kind,
551
+ subject,
552
+ before: change.before,
553
+ after: change.after,
554
+ classification: change.scope,
555
+ beforeObservationId,
556
+ afterObservationId,
557
+ evidence,
558
+ };
559
+ });
560
+ }
561
+ // --- explicit dependency evaluation -------------------------------------------
562
+ function extractTargetProperty(observation, targetName, property) {
563
+ const record = observation.targetEvidence[targetName];
564
+ if (!record)
565
+ return undefined;
566
+ const geometry = evidenceValue(record.geometry);
567
+ return geometry ? geometry[property] : undefined;
568
+ }
569
+ function evaluateTerm(before, after, direction, tolerance) {
570
+ const delta = after - before;
571
+ switch (direction) {
572
+ case 'increase':
573
+ return { matches: delta > tolerance, contradicts: delta < -tolerance };
574
+ case 'decrease':
575
+ return { matches: delta < -tolerance, contradicts: delta > tolerance };
576
+ case 'change':
577
+ return { matches: Math.abs(delta) > tolerance, contradicts: Math.abs(delta) <= tolerance };
578
+ case 'unchanged':
579
+ return { matches: Math.abs(delta) <= tolerance, contradicts: Math.abs(delta) > tolerance };
580
+ }
581
+ }
582
+ /**
583
+ * Evaluates only explicitly-declared dependencies (`config.expectedDependencies`,
584
+ * in declared order) - never synthesizes a declaration from observed
585
+ * co-change (Batch 1's hard non-causal boundary). Each declared side is
586
+ * evaluated independently from the target's own before/after geometry;
587
+ * `unavailable` when either side's required property evidence cannot be
588
+ * read, `contradictory-to-declaration` when either side's actual direction
589
+ * is the strict opposite of what was declared, `consistent` when both sides
590
+ * match their declared direction, `not-observed` otherwise. No causal claim
591
+ * is ever produced.
592
+ */
593
+ export function evaluateExpectedDependencies(before, after, declarations, tolerance) {
594
+ return declarations.map((declaration) => {
595
+ const causeBefore = extractTargetProperty(before, declaration.cause.target, declaration.cause.property);
596
+ const causeAfter = extractTargetProperty(after, declaration.cause.target, declaration.cause.property);
597
+ const effectBefore = extractTargetProperty(before, declaration.effect.target, declaration.effect.property);
598
+ const effectAfter = extractTargetProperty(after, declaration.effect.target, declaration.effect.property);
599
+ const supportingEvidence = [
600
+ { path: `targetEvidence.${declaration.cause.target}.geometry` },
601
+ { path: `targetEvidence.${declaration.effect.target}.geometry` },
602
+ ];
603
+ if (causeBefore === undefined || causeAfter === undefined || effectBefore === undefined || effectAfter === undefined) {
604
+ return { declaration, outcome: 'unavailable', supportingEvidence };
605
+ }
606
+ const causeEval = evaluateTerm(causeBefore, causeAfter, declaration.cause.direction, tolerance);
607
+ const effectEval = evaluateTerm(effectBefore, effectAfter, declaration.effect.direction, tolerance);
608
+ let outcome;
609
+ if (causeEval.contradicts || effectEval.contradicts) {
610
+ outcome = 'contradictory-to-declaration';
611
+ }
612
+ else if (causeEval.matches && effectEval.matches) {
613
+ outcome = 'consistent';
614
+ }
615
+ else {
616
+ outcome = 'not-observed';
617
+ }
618
+ return { declaration, outcome, supportingEvidence };
619
+ });
620
+ }
621
+ // --- comparison identity / source references ----------------------------------
622
+ function sourceReference(observation) {
623
+ const screenshot = evidenceValue(observation.screenshot);
624
+ if (!screenshot)
625
+ return undefined;
626
+ return {
627
+ observationId: observation.observationId,
628
+ requestId: observation.requestId,
629
+ producer: observation.producer,
630
+ observationSchemaVersion: observation.schemaVersion,
631
+ screenshot: { path: screenshot.path },
632
+ };
633
+ }
634
+ function normalizeConfig(config) {
635
+ return {
636
+ geometryTolerancePx: config.geometryTolerancePx ?? GEOMETRY_TOLERANCE_DEFAULT_PX,
637
+ ...(config.expectedDependencies !== undefined ? { expectedDependencies: config.expectedDependencies } : {}),
638
+ };
639
+ }
640
+ /**
641
+ * The one canonical pure comparison entry point (real Chromium ->
642
+ * ObservationArtifact -> relationship derivation -> **comparability ->
643
+ * comparison** -> ComparisonArtifact, see docs/ARCHITECTURE.md). Comparison
644
+ * is ordered: `compareObservations(a, b)` and `compareObservations(b, a)`
645
+ * produce different `comparisonRequestId`s and reversed-sign
646
+ * moved/resized deltas - `before`/`after` are never treated as an
647
+ * unordered pair.
648
+ */
649
+ export function compareObservations(before, after, configInput = {}) {
650
+ const beforeValidation = isValidObservationArtifact(before);
651
+ if (!beforeValidation.valid)
652
+ return { ok: false, reason: `before ObservationArtifact is invalid: ${beforeValidation.reason}` };
653
+ const afterValidation = isValidObservationArtifact(after);
654
+ if (!afterValidation.valid)
655
+ return { ok: false, reason: `after ObservationArtifact is invalid: ${afterValidation.reason}` };
656
+ const config = normalizeConfig(configInput);
657
+ if (!isValidComparisonConfig(config))
658
+ return { ok: false, reason: 'ComparisonConfig is invalid' };
659
+ if (!isValidGeometryTolerancePx(config.geometryTolerancePx))
660
+ return { ok: false, reason: 'ComparisonConfig.geometryTolerancePx is out of range' };
661
+ const beforeRef = sourceReference(before);
662
+ const afterRef = sourceReference(after);
663
+ if (!beforeRef)
664
+ return { ok: false, reason: 'before ObservationArtifact has no available screenshot evidence; cannot build a comparison source reference' };
665
+ if (!afterRef)
666
+ return { ok: false, reason: 'after ObservationArtifact has no available screenshot evidence; cannot build a comparison source reference' };
667
+ const tolerance = config.geometryTolerancePx;
668
+ const beforeRelResult = deriveLayoutRelationships(before, { geometryTolerancePx: tolerance });
669
+ if (!beforeRelResult.ok)
670
+ return { ok: false, reason: `relationship derivation failed for the before observation: ${beforeRelResult.reason}` };
671
+ const afterRelResult = deriveLayoutRelationships(after, { geometryTolerancePx: tolerance });
672
+ if (!afterRelResult.ok)
673
+ return { ok: false, reason: `relationship derivation failed for the after observation: ${afterRelResult.reason}` };
674
+ const relationshipsBefore = beforeRelResult.graph;
675
+ const relationshipsAfter = afterRelResult.graph;
676
+ const comparability = evaluateComparability(before, after);
677
+ const configurationChanges = compareTargetConfiguration(before, after);
678
+ const comparisonRequestId = buildComparisonRequestIdentity(before.observationId, after.observationId, config);
679
+ const comparisonId = buildComparisonIdentity(comparisonRequestId);
680
+ const shared = {
681
+ artifactKind: COMPARISON_ARTIFACT_KIND,
682
+ schemaVersion: COMPARISON_SCHEMA_VERSION,
683
+ comparisonId,
684
+ comparisonRequestId,
685
+ producer: getProducerInfo(),
686
+ provenance: { comparedAt: new Date().toISOString() },
687
+ before: beforeRef,
688
+ after: afterRef,
689
+ config,
690
+ comparability,
691
+ configurationChanges,
692
+ relationshipsBefore,
693
+ relationshipsAfter,
694
+ limits: { truncated: false, omittedFields: [], omittedTargetPairs: [] },
695
+ };
696
+ if (comparability.state === 'incomparable') {
697
+ const artifact = {
698
+ ...shared,
699
+ differences: [],
700
+ relationshipChanges: [],
701
+ expectedDependencyEvidence: [],
702
+ diagnostics: [],
703
+ };
704
+ const validation = isValidComparisonArtifact(artifact);
705
+ if (!validation.valid)
706
+ return { ok: false, reason: `assembled incomparable ComparisonArtifact failed structural validation: ${validation.reason}` };
707
+ return { ok: true, artifact };
708
+ }
709
+ const diagnostics = [];
710
+ const common = commonTargets(before, after);
711
+ const differences = [];
712
+ for (const target of common) {
713
+ differences.push(...compareOneTarget(before, after, target, tolerance, diagnostics));
714
+ }
715
+ const matched = common.filter((target) => targetPresence(before.targetEvidence[target.beforeName]) === 'matched' && targetPresence(after.targetEvidence[target.afterName]) === 'matched');
716
+ differences.push(...compareContainment(before, after, matched));
717
+ differences.push(...comparePageSize(before, after, tolerance));
718
+ differences.push(...compareScrollOwner(before, after));
719
+ const relationshipChanges = deriveRelationshipChanges(relationshipsBefore, relationshipsAfter, matched);
720
+ differences.push(...relationshipChangesToDifferences(relationshipChanges, before.observationId, after.observationId));
721
+ const expectedDependencyEvidence = evaluateExpectedDependencies(before, after, config.expectedDependencies ?? [], tolerance);
722
+ const artifact = {
723
+ ...shared,
724
+ differences,
725
+ relationshipChanges,
726
+ expectedDependencyEvidence,
727
+ diagnostics,
728
+ };
729
+ const validation = isValidComparisonArtifact(artifact);
730
+ if (!validation.valid)
731
+ return { ok: false, reason: `assembled ComparisonArtifact failed structural validation: ${validation.reason}` };
732
+ return { ok: true, artifact };
733
+ }
734
+ //# sourceMappingURL=comparisonEngine.js.map