diff --git a/docs/post-merge-reconciliation-2026-08-03.md b/docs/post-merge-reconciliation-2026-08-03.md new file mode 100644 index 0000000..f55d3bd --- /dev/null +++ b/docs/post-merge-reconciliation-2026-08-03.md @@ -0,0 +1,23 @@ +# Post-merge reconciliation — 2026-08-03 + +This repair exists because website PRs #35 and #36 merged with eleven unresolved review findings. + +## Repairs + +- Site-wide Markdown downloads preserve headings, links, lists, emphasis, code, and destinations. +- PDF exports remove the copied heading from the body so the synthetic title is not duplicated. +- Four Cuts `.m-title` and `.ref-title` vocabulary participates in labels and filenames. +- Linked cards reserve vertical space for the three export controls. +- Gonol receipt download waits for the current debounced and asynchronous state and intercepts the superseded downloader. +- Browser textarea CRLF/CR normalization is declared as information loss; no exact original-byte claim remains. +- Portable receipts use the merged strict UCNS schema, producer Git blob, complete false non-transfer boundary, derived-layer visibility, full-state SHA-256 receipt identity, and declared occurrence-vector dimension names. + +## Producer identities + +- UCNS machine contract: `9025957b4063c4748429cb56b52d3c9a56157c42`, blob `fdb94ff829a42c267de5f00f2a752550352a444d`. +- Strict receipt schema: `cfad06cf41bd1cc82861f08f6076f0d398a0089b`, blob `569ff5ce8a891a879314f2845905b7c9b8bc085c`, SHA-256 `2b15b1fbc517fdf11e617da8d31ae542cb6f240755ba12390a5bbc7d81034d56`. +- UCNS producer merge: `67964aac4cd34a0f6c3f83fd0da2dabef65b6c9d`. + +## hmmm + +The companion reconciliation layer is deliberately narrow: it converts the already-published browser representation into the strict portable receipt contract without claiming that the unresolved continuous Möbius frame, seven-form geometry, higher-gonol composition, English lexical floor, or embedding law has been supplied. diff --git a/package.json b/package.json index 30623e0..bfca4b1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "validate": "node scripts/validate-content.mjs && node scripts/verify-generated-routes.mjs && node scripts/verify-article-canon.mjs", "build": "npm run validate && eleventy && pagefind --site _site && node scripts/write-build-info.mjs", "pretest": "node scripts/prepare-tests.mjs", - "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs", + "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs", "test:generated": "node --test tests/generated-site.test.mjs tests/textbook-generated.test.mjs tests/math-generated.test.mjs && node tests/links.test.mjs", "test:browser": "playwright test", "test:e2e": "playwright test tests/site.spec.mjs", diff --git a/scripts/check-gonol-relationship-display-drift.mjs b/scripts/check-gonol-relationship-display-drift.mjs index bcdd805..f55399d 100644 --- a/scripts/check-gonol-relationship-display-drift.mjs +++ b/scripts/check-gonol-relationship-display-drift.mjs @@ -2,7 +2,7 @@ // id: gonol_relationship_display_drift_check // module_name: check-gonol-relationship-display-drift // module_kind: validator -// summary: verify the website consumes one exact commit-pinned UCNS relationship-display contract and receipt schema +// summary: verify the website consumes the exact merged UCNS relationship-display contract and strict receipt schema // owner: Erin Spencer // public_surface: validateGonolRelationshipPublication // internal_surface: readJson, sha256, assert @@ -11,17 +11,17 @@ // network_boundary: none; validation never follows a moving branch // user_data_boundary: none // admin_only: false -// tests: tests/gonol-relationships.test.mjs -// rollout: dependent publication consumer of the draft UCNS relationship-display contract +// tests: tests/gonol-relationships.test.mjs, tests/post-merge-reconciliation.test.mjs +// rollout: merged producer consumer with strict portable receipts // rollback: remove the artifact page, pinned data, validator, and route tests together // since: 2026-08-03 -// unresolved: producer merge order, seven-gonol geometry, continuous frame, English lexical floor, and embedding law +// unresolved: seven-gonol geometry, continuous frame, English lexical floor, and embedding law // === END MODULE_BUILD === // === CONTRACTS === // id: website_gonol_contract_pin_is_exact -// given: the local UCNS display contract and receipt schema are validated -// then: repository, commit, path, blob, SHA-256, primitive range, Public Gonol fixture, and unresolved standings match the declared publication pin +// given: the local UCNS display contract and strict receipt schema are validated +// then: repository, commits, paths, blobs, SHA-256 values, primitive range, Public Gonol fixture, and unresolved standings match the declared publication pin // class: evidence // since: 2026-08-03 // @@ -30,6 +30,12 @@ // then: unresolved geometry and represented-only comparison remain explicit without a consumer default // class: safety // since: 2026-08-03 +// +// id: website_gonol_portable_receipt_is_strict +// given: a relationship receipt leaves the browser page +// then: producer blob, candidate identity, pair order, display policy, losses, hmmm, and every non-transfer boundary remain schema-required +// class: evidence +// since: 2026-08-03 // === END CONTRACTS === import { createHash } from 'node:crypto'; @@ -60,6 +66,7 @@ export function validateGonolRelationshipPublication() { assert(publication.source.repository === 'The-Interdependency/ucns', 'source repository changed'); assert(publication.source.commit === '9025957b4063c4748429cb56b52d3c9a56157c42', 'source commit changed'); + assert(publication.source.merged_by === '67964aac4cd34a0f6c3f83fd0da2dabef65b6c9d', 'producer merge identity changed'); assert(publication.source.path === 'docs/gonol-relationship-display-v1.json', 'source path changed'); assert(publication.source.blob === 'fdb94ff829a42c267de5f00f2a752550352a444d', 'source blob changed'); assert(sha256(contractBytes) === publication.source.sha256, 'contract byte digest changed'); @@ -90,12 +97,36 @@ export function validateGonolRelationshipPublication() { assert(new Set(contract.public_gonol.space_manifestation_code_points).size === 25, 'SPACE manifestation pin is not unique'); assert(contract.public_gonol.space_manifestation_code_points.includes('U+0020'), 'U+0020 origin manifestation missing'); + assert(publication.receipt_schema.source_commit === 'cfad06cf41bd1cc82861f08f6076f0d398a0089b', 'strict schema source commit changed'); + assert(publication.receipt_schema.merged_by === '67964aac4cd34a0f6c3f83fd0da2dabef65b6c9d', 'strict schema merge identity changed'); + assert(publication.receipt_schema.blob === '569ff5ce8a891a879314f2845905b7c9b8bc085c', 'strict schema blob changed'); + assert(publication.receipt_schema.sha256 === '2b15b1fbc517fdf11e617da8d31ae542cb6f240755ba12390a5bbc7d81034d56', 'strict schema declared digest changed'); assert(sha256(schemaBytes) === publication.receipt_schema.sha256, 'receipt schema byte digest changed'); assert(receiptSchema.properties?.schema_id?.const === 'ucns.gonol-relationship-receipt', 'receipt schema identity changed'); assert(receiptSchema.properties?.schema_version?.const === '0.1.0', 'receipt schema version changed'); - assert(publication.authority_transfer === false, 'publication cannot receive authority'); - assert(publication.measurement_status_transfer === false, 'publication cannot receive measurement status'); - assert(publication.empirical_status_transfer === false, 'publication cannot receive empirical status'); + + const required = new Set(receiptSchema.required || []); + for (const field of ['candidate_identity', 'non_transfer', 'joint_context', 'provenance', 'information_loss', 'hmmm']) { + assert(required.has(field), `strict receipt no longer requires ${field}`); + } + const provenanceRequired = new Set(receiptSchema.properties?.provenance?.required || []); + assert(provenanceRequired.has('contract_blob'), 'portable provenance no longer requires producer blob'); + assert(receiptSchema.$defs?.operand?.properties?.native_scale?.properties?.numeric_rank?.const === null, 'numeric scale rank became available without a scale law'); + assert(receiptSchema.$defs?.displayPolicy?.properties?.parameters?.required?.includes('derived_layers_visible'), 'derived-layer visibility is not receipt-bearing'); + assert(receiptSchema.$defs?.pairAB && receiptSchema.$defs?.pairBC && receiptSchema.$defs?.pairCA, 'declared pair identities are not fixed'); + + const nonTransferRequired = new Set(receiptSchema.$defs?.nonTransfer?.required || []); + for (const field of [ + 'authority_transfer', 'semantic_authority_transfer', 'proof_status_transfer', + 'certification_status_transfer', 'measurement_status_transfer', 'empirical_status_transfer', + 'completion_status_transfer', 'embedding_status_transfer', 'producer_authentication_transfer' + ]) { + assert(nonTransferRequired.has(field), `non-transfer boundary missing: ${field}`); + assert(receiptSchema.$defs.nonTransfer.properties[field].const === false, `non-transfer boundary is not false: ${field}`); + assert(publication[field] === false || (field === 'producer_authentication_transfer' && publication.producer_authenticated === false), `publication boundary changed: ${field}`); + } + + assert(publication.publication_status === 'merged-producer-reconciled-consumer', 'publication status is not reconciled'); assert(publication.producer_authenticated === false, 'content identity cannot claim producer authentication'); return { @@ -108,5 +139,5 @@ export function validateGonolRelationshipPublication() { const result = validateGonolRelationshipPublication(); if (process.argv[1]?.endsWith('check-gonol-relationship-display-drift.mjs')) { - console.log(`gonol relationship publication: pinned ${result.contractSha256}; primitives ${result.primitiveArities.join('/')}; Public Gonol ${result.publicGonolArity}`); + console.log(`gonol relationship publication: contract ${result.contractSha256}; strict schema ${result.receiptSchemaSha256}; primitives ${result.primitiveArities.join('/')}; Public Gonol ${result.publicGonolArity}`); } diff --git a/src/_data/gonol_relationship_publication.json b/src/_data/gonol_relationship_publication.json index 06de7d6..50fa6b3 100644 --- a/src/_data/gonol_relationship_publication.json +++ b/src/_data/gonol_relationship_publication.json @@ -2,6 +2,7 @@ "source": { "repository": "The-Interdependency/ucns", "commit": "9025957b4063c4748429cb56b52d3c9a56157c42", + "merged_by": "67964aac4cd34a0f6c3f83fd0da2dabef65b6c9d", "path": "docs/gonol-relationship-display-v1.json", "blob": "fdb94ff829a42c267de5f00f2a752550352a444d", "sha256": "d1ac67d4a5d5b4cd0622056af94b24a1404f1f5c7d7dfeb59ee28e5edb73aad5", @@ -10,15 +11,21 @@ "receipt_schema": { "path": "/assets/data/gonol-relationship-receipt-v1.schema.json", "source_path": "docs/gonol-relationship-receipt-v1.schema.json", - "blob": "d83e4423612160a8a500e4d5bc4a9379bd4260b8", - "sha256": "a6565d92e233768fd306f21f2a40a0f38867b32c5740e26fd299c0e23af1d9bf" + "source_commit": "cfad06cf41bd1cc82861f08f6076f0d398a0089b", + "merged_by": "67964aac4cd34a0f6c3f83fd0da2dabef65b6c9d", + "blob": "569ff5ce8a891a879314f2845905b7c9b8bc085c", + "sha256": "2b15b1fbc517fdf11e617da8d31ae542cb6f240755ba12390a5bbc7d81034d56" }, "producer_pr": "https://github.com/The-Interdependency/ucns/pull/168", - "publication_status": "dependent-draft-consumer", + "publication_status": "merged-producer-reconciled-consumer", "authority_transfer": false, + "semantic_authority_transfer": false, "proof_status_transfer": false, + "certification_status_transfer": false, "measurement_status_transfer": false, "empirical_status_transfer": false, + "completion_status_transfer": false, + "embedding_status_transfer": false, "producer_authenticated": false, "hmmm": "The seven-gonol geometry, continuous moving frame, cross-scope composition, completed English lexical floor, root policy, and hyperdimensional embedding law remain unresolved." } diff --git a/src/assets/data/gonol-relationship-receipt-v1.schema.json b/src/assets/data/gonol-relationship-receipt-v1.schema.json index d83e442..569ff5c 100644 --- a/src/assets/data/gonol-relationship-receipt-v1.schema.json +++ b/src/assets/data/gonol-relationship-receipt-v1.schema.json @@ -1,234 +1 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://theinterdependentway.org/schemas/ucns/gonol-relationship-receipt-v1.schema.json", - "title": "UCNS gonol relationship receipt", - "description": "Option-preserving evidence receipt for the declared one-, two-, three-, and seven-gonol display primitives.", - "type": "object", - "additionalProperties": false, - "required": [ - "schema_id", - "schema_version", - "receipt_id", - "primitive_arity", - "operands", - "display_policy", - "pairing_plan", - "pairwise_receipts", - "evidence_status", - "provenance", - "information_loss", - "hmmm" - ], - "properties": { - "schema_id": {"const": "ucns.gonol-relationship-receipt"}, - "schema_version": {"const": "0.1.0"}, - "receipt_id": {"type": "string", "minLength": 1}, - "primitive_arity": {"enum": [1, 2, 3, 7]}, - "operands": { - "type": "array", - "minItems": 1, - "maxItems": 7, - "items": {"$ref": "#/$defs/operand"} - }, - "display_policy": {"$ref": "#/$defs/policy"}, - "pairing_plan": { - "type": "object", - "additionalProperties": false, - "required": ["plan_id", "plan_version", "pairs", "standing"], - "properties": { - "plan_id": {"type": "string", "minLength": 1}, - "plan_version": {"type": "string", "minLength": 1}, - "pairs": { - "type": "array", - "items": { - "type": "array", - "prefixItems": [ - {"type": "string", "minLength": 1}, - {"type": "string", "minLength": 1} - ], - "items": false, - "minItems": 2, - "maxItems": 2 - } - }, - "standing": {"enum": ["declared-static-projection", "hmmm-unresolved"]} - } - }, - "pairwise_receipts": { - "type": "array", - "items": {"$ref": "#/$defs/pairwiseReceipt"} - }, - "joint_context": { - "oneOf": [ - {"type": "null"}, - { - "type": "object", - "additionalProperties": false, - "required": ["standing", "evidence", "promoted_to_gonol"], - "properties": { - "standing": {"enum": ["represented-evidence", "hmmm-unresolved"]}, - "evidence": {"type": "array", "items": {"type": "string"}}, - "promoted_to_gonol": {"const": false} - } - } - ] - }, - "evidence_status": {"enum": ["represented-evidence", "candidate-measured-evidence"]}, - "provenance": { - "type": "object", - "additionalProperties": false, - "required": ["contract_repository", "contract_commit", "contract_path", "contract_sha256", "producer_authenticated"], - "properties": { - "contract_repository": {"const": "The-Interdependency/ucns"}, - "contract_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, - "contract_path": {"const": "docs/gonol-relationship-display-v1.json"}, - "contract_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, - "producer_authenticated": {"const": false}, - "consumer_reference": {"type": ["string", "null"]} - } - }, - "information_loss": {"type": "array", "items": {"type": "string", "minLength": 1}}, - "hmmm": {"type": "array", "items": {"type": "string", "minLength": 1}} - }, - "allOf": [ - { - "if": {"properties": {"primitive_arity": {"const": 1}}}, - "then": { - "properties": { - "operands": {"minItems": 1, "maxItems": 1}, - "pairwise_receipts": {"minItems": 0, "maxItems": 0}, - "pairing_plan": { - "properties": {"pairs": {"minItems": 0, "maxItems": 0}} - } - } - } - }, - { - "if": {"properties": {"primitive_arity": {"const": 2}}}, - "then": { - "properties": { - "operands": {"minItems": 2, "maxItems": 2}, - "pairwise_receipts": {"minItems": 1, "maxItems": 1}, - "pairing_plan": { - "properties": {"pairs": {"minItems": 1, "maxItems": 1}} - } - } - } - }, - { - "if": {"properties": {"primitive_arity": {"const": 3}}}, - "then": { - "properties": { - "operands": {"minItems": 3, "maxItems": 3}, - "pairwise_receipts": {"minItems": 3, "maxItems": 3}, - "pairing_plan": { - "properties": {"pairs": {"minItems": 3, "maxItems": 3}} - } - } - } - }, - { - "if": {"properties": {"primitive_arity": {"const": 7}}}, - "then": { - "properties": { - "operands": {"minItems": 7, "maxItems": 7}, - "pairing_plan": { - "properties": {"standing": {"const": "hmmm-unresolved"}} - } - } - } - } - ], - "$defs": { - "operand": { - "type": "object", - "additionalProperties": false, - "required": [ - "operand_id", - "gonol_id", - "occurrence_ordinal", - "source_identity", - "content_adapter", - "content_digest", - "native_scale", - "payload_reference", - "evidence_status", - "hmmm" - ], - "properties": { - "operand_id": {"type": "string", "minLength": 1}, - "gonol_id": {"type": "string", "minLength": 1}, - "occurrence_ordinal": {"type": "integer", "minimum": 0}, - "source_identity": {"type": "string", "minLength": 1}, - "content_adapter": { - "type": "object", - "additionalProperties": false, - "required": ["name", "version"], - "properties": { - "name": {"type": "string", "minLength": 1}, - "version": {"type": "string", "minLength": 1} - } - }, - "content_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, - "native_scale": { - "type": "object", - "additionalProperties": false, - "required": ["scale_id", "declaration", "numeric_rank"], - "properties": { - "scale_id": {"type": "string", "minLength": 1}, - "declaration": {"type": "string", "minLength": 1}, - "numeric_rank": {"type": ["number", "null"]} - } - }, - "payload_reference": {"type": "string", "minLength": 1}, - "retained_structure_reference": {"type": ["string", "null"]}, - "trajectory_reference": {"type": ["string", "null"]}, - "evidence_status": {"enum": ["represented-evidence", "candidate-measured-evidence"]}, - "hmmm": {"type": "array", "items": {"type": "string", "minLength": 1}} - } - }, - "policy": { - "type": "object", - "additionalProperties": false, - "required": ["policy_id", "policy_version", "standing", "parameters", "information_loss"], - "properties": { - "policy_id": {"type": "string", "minLength": 1}, - "policy_version": {"type": "string", "minLength": 1}, - "standing": {"enum": ["unresolved", "experiment-candidate", "declared-static-projection"]}, - "parameters": {"type": "object"}, - "information_loss": {"type": "array", "items": {"type": "string", "minLength": 1}} - } - }, - "pairwiseReceipt": { - "type": "object", - "additionalProperties": false, - "required": [ - "pair_id", - "left_operand_id", - "right_operand_id", - "relationship_status", - "comparison_policy", - "comparison_outcome", - "evidence", - "information_loss", - "hmmm" - ], - "properties": { - "pair_id": {"type": "string", "minLength": 1}, - "left_operand_id": {"type": "string", "minLength": 1}, - "right_operand_id": {"type": "string", "minLength": 1}, - "relationship_status": {"enum": ["represented-evidence", "candidate-measured-evidence"]}, - "comparison_policy": { - "oneOf": [ - {"type": "null"}, - {"$ref": "#/$defs/policy"} - ] - }, - "comparison_outcome": {}, - "evidence": {"type": "array", "items": {"type": "string", "minLength": 1}}, - "information_loss": {"type": "array", "items": {"type": "string", "minLength": 1}}, - "hmmm": {"type": "array", "items": {"type": "string", "minLength": 1}} - } - } - } -} +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://theinterdependentway.org/schemas/ucns/gonol-relationship-receipt-v1.schema.json","title":"UCNS gonol relationship receipt","description":"Option-preserving evidence receipt for the declared one-, two-, three-, and seven-gonol display primitives.","type":"object","additionalProperties":false,"required":["schema_id","schema_version","receipt_id","primitive_arity","operands","display_policy","pairing_plan","pairwise_receipts","joint_context","evidence_status","candidate_identity","provenance","non_transfer","information_loss","hmmm"],"properties":{"schema_id":{"const":"ucns.gonol-relationship-receipt"},"schema_version":{"const":"0.1.0"},"receipt_id":{"type":"string","minLength":1},"primitive_arity":{"enum":[1,2,3,7]},"operands":{"type":"array","minItems":1,"maxItems":7,"items":{"$ref":"#/$defs/operand"}},"display_policy":{"$ref":"#/$defs/displayPolicy"},"pairing_plan":{"$ref":"#/$defs/pairingPlan"},"pairwise_receipts":{"type":"array","items":{"$ref":"#/$defs/pairwiseReceipt"}},"joint_context":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["standing","evidence","promoted_to_gonol"],"properties":{"standing":{"enum":["represented-evidence","hmmm-unresolved"]},"evidence":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true},"promoted_to_gonol":{"const":false}}}]},"evidence_status":{"enum":["represented-evidence","candidate-measured-evidence"]},"candidate_identity":{"oneOf":[{"type":"null"},{"$ref":"#/$defs/candidateIdentity"}]},"provenance":{"type":"object","additionalProperties":false,"required":["contract_repository","contract_commit","contract_path","contract_blob","contract_sha256","producer_authenticated","consumer_reference"],"properties":{"contract_repository":{"const":"The-Interdependency/ucns"},"contract_commit":{"type":"string","pattern":"^[0-9a-f]{40}$"},"contract_path":{"const":"docs/gonol-relationship-display-v1.json"},"contract_blob":{"type":"string","pattern":"^[0-9a-f]{40}$"},"contract_sha256":{"type":"string","pattern":"^[0-9a-f]{64}$"},"producer_authenticated":{"const":false},"consumer_reference":{"type":["string","null"]}}},"non_transfer":{"$ref":"#/$defs/nonTransfer"},"information_loss":{"type":"array","minItems":2,"uniqueItems":true,"items":{"type":"string","minLength":1},"allOf":[{"contains":{"const":"native-scale magnitude is not encoded by visible radius"},"minContains":1},{"contains":{"const":"continuous Mobius band width and local-frame motion are not encoded by the centerline projection"},"minContains":1}]},"hmmm":{"type":"array","minItems":5,"uniqueItems":true,"items":{"type":"string","minLength":1},"allOf":[{"contains":{"const":"continuous local-frame geometry across the Mobius band"},"minContains":1},{"contains":{"const":"seven-gonol display geometry and pairing plan"},"minContains":1},{"contains":{"const":"promotion law for derived scope or relationship circles"},"minContains":1},{"contains":{"const":"cross-scope and higher-gonol composition"},"minContains":1},{"contains":{"const":"English root policy, lexical source completion, and hyperdimensional embedding law"},"minContains":1}]}},"allOf":[{"if":{"properties":{"primitive_arity":{"const":1}},"required":["primitive_arity"]},"then":{"properties":{"operands":{"minItems":1,"maxItems":1,"prefixItems":[{"$ref":"#/$defs/operandA"}],"items":false},"pairing_plan":{"properties":{"plan_id":{"const":"ucns.pairing.none-one"},"standing":{"const":"declared-static-projection"},"pairs":{"minItems":0,"maxItems":0,"items":false}}},"pairwise_receipts":{"minItems":0,"maxItems":0,"items":false},"joint_context":{"type":"null"}}}},{"if":{"properties":{"primitive_arity":{"const":2}},"required":["primitive_arity"]},"then":{"properties":{"operands":{"minItems":2,"maxItems":2,"prefixItems":[{"$ref":"#/$defs/operandA"},{"$ref":"#/$defs/operandB"}],"items":false},"pairing_plan":{"properties":{"plan_id":{"const":"ucns.pairing.vesica-two"},"standing":{"const":"declared-static-projection"},"pairs":{"minItems":1,"maxItems":1,"items":false,"prefixItems":[{"const":["A","B"]}]}}},"pairwise_receipts":{"minItems":1,"maxItems":1,"items":false,"prefixItems":[{"$ref":"#/$defs/pairAB"}]},"joint_context":{"type":"null"}}}},{"if":{"properties":{"primitive_arity":{"const":3}},"required":["primitive_arity"]},"then":{"properties":{"operands":{"minItems":3,"maxItems":3,"prefixItems":[{"$ref":"#/$defs/operandA"},{"$ref":"#/$defs/operandB"},{"$ref":"#/$defs/operandC"}],"items":false},"pairing_plan":{"properties":{"plan_id":{"const":"ucns.pairing.triquetra-three"},"standing":{"const":"declared-static-projection"},"pairs":{"minItems":3,"maxItems":3,"items":false,"prefixItems":[{"const":["A","B"]},{"const":["B","C"]},{"const":["C","A"]}]}}},"pairwise_receipts":{"minItems":3,"maxItems":3,"items":false,"prefixItems":[{"$ref":"#/$defs/pairAB"},{"$ref":"#/$defs/pairBC"},{"$ref":"#/$defs/pairCA"}]}}}},{"if":{"properties":{"primitive_arity":{"const":7}},"required":["primitive_arity"]},"then":{"properties":{"operands":{"minItems":7,"maxItems":7,"prefixItems":[{"$ref":"#/$defs/operandA"},{"$ref":"#/$defs/operandB"},{"$ref":"#/$defs/operandC"},{"$ref":"#/$defs/operandD"},{"$ref":"#/$defs/operandE"},{"$ref":"#/$defs/operandF"},{"$ref":"#/$defs/operandG"}],"items":false},"pairing_plan":{"properties":{"plan_id":{"const":"ucns.pairing.full-seven-unresolved"},"standing":{"const":"hmmm-unresolved"},"pairs":{"minItems":0,"maxItems":0,"items":false}}},"pairwise_receipts":{"minItems":0,"maxItems":0,"items":false},"joint_context":{"type":"null"}}}},{"if":{"properties":{"evidence_status":{"const":"represented-evidence"}},"required":["evidence_status"]},"then":{"properties":{"candidate_identity":{"type":"null"},"operands":{"items":{"properties":{"evidence_status":{"const":"represented-evidence"}}}},"pairwise_receipts":{"items":{"properties":{"relationship_status":{"const":"represented-evidence"}}}}}}},{"if":{"properties":{"evidence_status":{"const":"candidate-measured-evidence"}},"required":["evidence_status"]},"then":{"properties":{"candidate_identity":{"$ref":"#/$defs/candidateIdentity"}}}},{"if":{"anyOf":[{"properties":{"operands":{"contains":{"properties":{"evidence_status":{"const":"candidate-measured-evidence"}},"required":["evidence_status"]}}}},{"properties":{"pairwise_receipts":{"contains":{"properties":{"relationship_status":{"const":"candidate-measured-evidence"}},"required":["relationship_status"]}}}}]},"then":{"properties":{"evidence_status":{"const":"candidate-measured-evidence"},"candidate_identity":{"$ref":"#/$defs/candidateIdentity"}}}}],"$defs":{"operand":{"type":"object","additionalProperties":false,"required":["operand_slot","operand_id","gonol_id","occurrence_ordinal","source_identity","content_adapter","content_digest","native_scale","payload_reference","evidence_status","hmmm"],"properties":{"operand_slot":{"enum":["A","B","C","D","E","F","G"]},"operand_id":{"type":"string","minLength":3},"gonol_id":{"type":"string","minLength":1},"occurrence_ordinal":{"type":"integer","minimum":0},"source_identity":{"type":"string","minLength":1},"content_adapter":{"type":"object","additionalProperties":false,"required":["name","version"],"properties":{"name":{"type":"string","minLength":1},"version":{"type":"string","minLength":1}}},"content_digest":{"oneOf":[{"type":"null"},{"type":"string","pattern":"^[0-9a-f]{64}$"}]},"native_scale":{"type":"object","additionalProperties":false,"required":["scale_id","declaration","numeric_rank"],"properties":{"scale_id":{"type":"string","minLength":1},"declaration":{"type":"string","minLength":1},"numeric_rank":{"const":null}}},"payload_reference":{"type":"string","minLength":1},"retained_structure_reference":{"type":["string","null"]},"trajectory_reference":{"type":["string","null"]},"evidence_status":{"enum":["represented-evidence","candidate-measured-evidence"]},"hmmm":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}}},"operandA":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"A"},"operand_id":{"type":"string","pattern":"^A:.+"}}}]},"operandB":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"B"},"operand_id":{"type":"string","pattern":"^B:.+"}}}]},"operandC":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"C"},"operand_id":{"type":"string","pattern":"^C:.+"}}}]},"operandD":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"D"},"operand_id":{"type":"string","pattern":"^D:.+"}}}]},"operandE":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"E"},"operand_id":{"type":"string","pattern":"^E:.+"}}}]},"operandF":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"F"},"operand_id":{"type":"string","pattern":"^F:.+"}}}]},"operandG":{"allOf":[{"$ref":"#/$defs/operand"},{"properties":{"operand_slot":{"const":"G"},"operand_id":{"type":"string","pattern":"^G:.+"}}}]},"displayPolicy":{"type":"object","additionalProperties":false,"required":["policy_id","policy_version","standing","parameters","information_loss"],"properties":{"policy_id":{"const":"ucns.display.relation-equalized"},"policy_version":{"const":"0.1.0"},"standing":{"const":"declared-static-projection"},"parameters":{"type":"object","additionalProperties":false,"required":["display_radius","native_scale_retained","visible_radius_represents_native_scale","derived_layers_visible"],"properties":{"display_radius":{"const":1},"native_scale_retained":{"const":true},"visible_radius_represents_native_scale":{"const":false},"derived_layers_visible":{"type":"boolean"}}},"information_loss":{"type":"array","minItems":2,"uniqueItems":true,"items":{"type":"string","minLength":1},"allOf":[{"contains":{"const":"native-scale magnitude is not encoded by visible radius"},"minContains":1},{"contains":{"const":"continuous Mobius band width and local-frame motion are not encoded by the centerline projection"},"minContains":1}]}}},"pairingPlan":{"type":"object","additionalProperties":false,"required":["plan_id","plan_version","pairs","standing"],"properties":{"plan_id":{"type":"string","minLength":1},"plan_version":{"const":"0.1.0"},"pairs":{"type":"array","items":{"type":"array","prefixItems":[{"enum":["A","B","C","D","E","F","G"]},{"enum":["A","B","C","D","E","F","G"]}],"items":false,"minItems":2,"maxItems":2}},"standing":{"enum":["declared-static-projection","hmmm-unresolved"]}}},"candidateIdentity":{"type":"object","additionalProperties":false,"required":["name","evaluator_kind","version","code_reference","scope","policy_dependencies"],"properties":{"name":{"type":"string","minLength":1},"evaluator_kind":{"type":"string","minLength":1},"version":{"type":"string","minLength":1},"code_reference":{"type":"string","minLength":1},"scope":{"type":"string","minLength":1},"policy_dependencies":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}}},"comparisonPolicy":{"type":"object","additionalProperties":false,"required":["policy_id","policy_version","standing","parameters","information_loss"],"properties":{"policy_id":{"type":"string","minLength":1},"policy_version":{"type":"string","minLength":1},"standing":{"const":"experiment-candidate"},"parameters":{"type":"object"},"information_loss":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}}},"pairwiseReceipt":{"type":"object","additionalProperties":false,"required":["pair_id","left_operand_slot","right_operand_slot","relationship_status","comparison_policy","comparison_outcome","evidence","information_loss","hmmm"],"properties":{"pair_id":{"type":"string","minLength":1},"left_operand_slot":{"enum":["A","B","C","D","E","F","G"]},"right_operand_slot":{"enum":["A","B","C","D","E","F","G"]},"relationship_status":{"enum":["represented-evidence","candidate-measured-evidence"]},"comparison_policy":{"oneOf":[{"type":"null"},{"$ref":"#/$defs/comparisonPolicy"}]},"comparison_outcome":{},"evidence":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}},"information_loss":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}},"hmmm":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}},"allOf":[{"if":{"properties":{"relationship_status":{"const":"represented-evidence"}},"required":["relationship_status"]},"then":{"properties":{"comparison_policy":{"type":"null"},"comparison_outcome":{"type":"null"}}}},{"if":{"properties":{"relationship_status":{"const":"candidate-measured-evidence"}},"required":["relationship_status"]},"then":{"properties":{"comparison_policy":{"$ref":"#/$defs/comparisonPolicy"},"comparison_outcome":{"not":{"type":"null"}}}}}]},"pairAB":{"allOf":[{"$ref":"#/$defs/pairwiseReceipt"},{"properties":{"pair_id":{"const":"A-B"},"left_operand_slot":{"const":"A"},"right_operand_slot":{"const":"B"}}}]},"pairBC":{"allOf":[{"$ref":"#/$defs/pairwiseReceipt"},{"properties":{"pair_id":{"const":"B-C"},"left_operand_slot":{"const":"B"},"right_operand_slot":{"const":"C"}}}]},"pairCA":{"allOf":[{"$ref":"#/$defs/pairwiseReceipt"},{"properties":{"pair_id":{"const":"C-A"},"left_operand_slot":{"const":"C"},"right_operand_slot":{"const":"A"}}}]},"nonTransfer":{"type":"object","additionalProperties":false,"required":["authority_transfer","semantic_authority_transfer","proof_status_transfer","certification_status_transfer","measurement_status_transfer","empirical_status_transfer","completion_status_transfer","embedding_status_transfer","producer_authentication_transfer"],"properties":{"authority_transfer":{"const":false},"semantic_authority_transfer":{"const":false},"proof_status_transfer":{"const":false},"certification_status_transfer":{"const":false},"measurement_status_transfer":{"const":false},"empirical_status_transfer":{"const":false},"completion_status_transfer":{"const":false},"embedding_status_transfer":{"const":false},"producer_authentication_transfer":{"const":false}}}}} \ No newline at end of file diff --git a/src/assets/js/gonol-reconciliation.js b/src/assets/js/gonol-reconciliation.js new file mode 100644 index 0000000..1fdedba --- /dev/null +++ b/src/assets/js/gonol-reconciliation.js @@ -0,0 +1,309 @@ +// === MODULE_BUILD === +// id: public_gonol_receipt_reconciliation +// purpose: Reconcile the published Gonol lab with the merged strict UCNS receipt schema and intercept stale downloads. +// entrypoint: dynamically imported by site.js only on the Gonol relationship lab +// tests: tests/post-merge-reconciliation.test.mjs, tests/gonol-relationships.test.mjs +// === END MODULE_BUILD === + +const lab = document.querySelector('[data-gonol-lab]'); +if (!lab) throw new Error('Gonol reconciliation loaded without a relationship lab.'); + +const receiptOutput = lab.querySelector('[data-receipt-output]'); +const vectorOutput = lab.querySelector('[data-vector-output]'); +const downloadButton = lab.querySelector('[data-download-receipt]'); +const statusOutput = lab.querySelector('[data-gonol-status]'); +const comparisonControl = lab.querySelector('[data-comparison-policy]'); +const derivedLayersControl = lab.querySelector('[data-derived-layers]'); +const publication = JSON.parse(document.querySelector('#gonol-relationship-publication').textContent); + +const REQUIRED_LOSSES = [ + 'native-scale magnitude is not encoded by visible radius', + 'continuous Mobius band width and local-frame motion are not encoded by the centerline projection' +]; +const REQUIRED_HMMM = [ + 'continuous local-frame geometry across the Mobius band', + 'seven-gonol display geometry and pairing plan', + 'promotion law for derived scope or relationship circles', + 'cross-scope and higher-gonol composition', + 'English root policy, lexical source completion, and hyperdimensional embedding law' +]; +const NON_TRANSFER = { + authority_transfer: false, + semantic_authority_transfer: false, + proof_status_transfer: false, + certification_status_transfer: false, + measurement_status_transfer: false, + empirical_status_transfer: false, + completion_status_transfer: false, + embedding_status_transfer: false, + producer_authentication_transfer: false +}; +const PAIR_PLAN = { + 1: { plan_id: 'ucns.pairing.none-one', pairs: [] }, + 2: { plan_id: 'ucns.pairing.vesica-two', pairs: [['A', 'B']] }, + 3: { plan_id: 'ucns.pairing.triquetra-three', pairs: [['A', 'B'], ['B', 'C'], ['C', 'A']] }, + 7: { plan_id: 'ucns.pairing.full-seven-unresolved', pairs: [] } +}; + +let inputRevision = 0; +let reconciledRevision = -1; +let reconciling = false; + +function canonical(value) { + if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`; + } + return JSON.stringify(value); +} + +async function sha256(value) { + const bytes = new TextEncoder().encode(value); + const digest = await crypto.subtle.digest('SHA-256', bytes); + return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); +} + +function activeArity() { + return Number(lab.querySelector('input[name="gonol-arity"]:checked')?.value || 2); +} + +function normalizeOperand(operand, index) { + const slot = String.fromCharCode(65 + index); + const hmmm = Array.from(new Set([ + ...(operand.hmmm || []), + 'browser textarea values normalize CRLF and CR line endings to LF before UTF-8 hashing' + ])); + return { + operand_slot: slot, + operand_id: `${slot}:${operand.gonol_id || operand.operand_id || 'hmmm-unset'}`, + gonol_id: operand.gonol_id || `hmmm-unset-gonol-${slot}`, + occurrence_ordinal: Number.isInteger(operand.occurrence_ordinal) ? operand.occurrence_ordinal : index, + source_identity: operand.source_identity || `hmmm-unset-source-${slot}`, + content_adapter: { + name: 'website.browser-textarea-lf-utf8', + version: '0.2.0' + }, + content_digest: operand.content_digest || null, + native_scale: { + scale_id: operand.native_scale?.scale_id || `hmmm-unset-scale-${slot}`, + declaration: operand.native_scale?.declaration || operand.native_scale?.scale_id || `hmmm-unset-scale-${slot}`, + numeric_rank: null + }, + payload_reference: operand.payload_reference || `interactive-session:hmmm-unresolved:${slot}`, + retained_structure_reference: operand.retained_structure_reference ?? null, + trajectory_reference: operand.trajectory_reference ?? null, + evidence_status: 'represented-evidence', + hmmm + }; +} + +function normalizePair(pair, index, plan) { + const [left, right] = plan.pairs[index]; + const measured = pair.relationship_status === 'candidate-measured-evidence'; + const policy = measured ? { + policy_id: 'website.comparison.browser-textarea-lf-utf8', + policy_version: '0.2.0', + standing: 'experiment-candidate', + parameters: { + adapter: 'website.browser-textarea-lf-utf8/0.2.0', + normalization: 'html-textarea-value-crlf-and-cr-to-lf' + }, + information_loss: [ + 'original CRLF or CR line-ending bytes are not recoverable from an HTML textarea value', + 'comparison does not establish structural, semantic, geometric, native-scale, or completion equivalence' + ] + } : null; + return { + pair_id: `${left}-${right}`, + left_operand_slot: left, + right_operand_slot: right, + relationship_status: measured ? 'candidate-measured-evidence' : 'represented-evidence', + comparison_policy: policy, + comparison_outcome: measured ? pair.comparison_outcome : null, + evidence: pair.evidence?.length ? pair.evidence : [`${left}↔${right} remains a distinct relationship occurrence`], + information_loss: pair.information_loss?.length ? pair.information_loss : REQUIRED_LOSSES, + hmmm: pair.hmmm?.length ? pair.hmmm : ['pairwise display evidence does not select a comparison or completion law'] + }; +} + +async function reconcileReceipt(raw) { + const arity = Number(raw.primitive_arity || activeArity()); + const plan = PAIR_PLAN[arity]; + if (!plan) throw new Error(`Unsupported primitive arity ${arity}.`); + const operands = (raw.operands || []).slice(0, arity).map(normalizeOperand); + const pairwise = (raw.pairwise_receipts || []).slice(0, plan.pairs.length) + .map((pair, index) => normalizePair(pair, index, plan)); + const candidateMeasured = pairwise.some(pair => pair.relationship_status === 'candidate-measured-evidence'); + const receipt = { + schema_id: 'ucns.gonol-relationship-receipt', + schema_version: '0.1.0', + receipt_id: 'pending', + primitive_arity: arity, + operands, + display_policy: { + policy_id: 'ucns.display.relation-equalized', + policy_version: '0.1.0', + standing: 'declared-static-projection', + parameters: { + display_radius: 1, + native_scale_retained: true, + visible_radius_represents_native_scale: false, + derived_layers_visible: Boolean(derivedLayersControl?.checked) + }, + information_loss: REQUIRED_LOSSES + }, + pairing_plan: { + plan_id: plan.plan_id, + plan_version: '0.1.0', + pairs: plan.pairs, + standing: arity === 7 ? 'hmmm-unresolved' : 'declared-static-projection' + }, + pairwise_receipts: pairwise, + joint_context: arity === 3 ? { + standing: 'represented-evidence', + evidence: ['central three-way overlap remains joint context beside all three pairwise vesicas'], + promoted_to_gonol: false + } : null, + evidence_status: candidateMeasured ? 'candidate-measured-evidence' : 'represented-evidence', + candidate_identity: candidateMeasured ? { + name: 'website.browser-textarea-lf-utf8-equality', + evaluator_kind: 'browser-local-exact-digest-comparison', + version: '0.2.0', + code_reference: 'The-Interdependency/The-Interdependency.github.io:src/assets/js/gonol-reconciliation.js', + scope: 'active entered operands in one local browser relationship receipt', + policy_dependencies: ['website.comparison.browser-textarea-lf-utf8/0.2.0'] + } : null, + provenance: { + contract_repository: publication.source.repository, + contract_commit: publication.source.commit, + contract_path: publication.source.path, + contract_blob: publication.source.blob, + contract_sha256: publication.source.sha256, + producer_authenticated: false, + consumer_reference: 'The-Interdependency/The-Interdependency.github.io:/artifacts/gonol-relationships/' + }, + non_transfer: NON_TRANSFER, + information_loss: REQUIRED_LOSSES, + hmmm: REQUIRED_HMMM + }; + const identity = { ...receipt }; + delete identity.receipt_id; + receipt.receipt_id = `website-gonol-relation:sha256:${await sha256(canonical(identity))}`; + return receipt; +} + +function reconcileVector() { + try { + const vector = JSON.parse(vectorOutput.textContent); + if (!Array.isArray(vector.occurrences)) return; + vector.occurrences = vector.occurrences.map(occurrence => ({ + source_ordinal: occurrence.source_ordinal, + unicode_scalar_value: occurrence.source_value?.codePointAt(0) ?? null, + carrier_position_or_null: occurrence.carrier_position ?? null, + source_value: occurrence.source_value, + role: occurrence.role, + instantiates_vertex: occurrence.instantiates_vertex, + carrier_assignment: occurrence.carrier_assignment + })); + vectorOutput.textContent = JSON.stringify(vector, null, 2); + } catch { + // The producer module may still be assembling its first vector. + } +} + +function updatePairOutputs(receipt) { + const byId = new Map(receipt.pairwise_receipts.map(pair => [pair.pair_id.replace('-', ''), pair])); + lab.querySelectorAll('[data-pair-receipt]').forEach(element => { + const pair = element.closest('[data-pair]')?.dataset.pair; + element.textContent = byId.has(pair) + ? JSON.stringify(byId.get(pair), null, 2) + : 'No pairwise receipt belongs to the selected primitive.'; + }); +} + +async function reconcileVisibleReceipt() { + if (reconciling) return; + let raw; + try { + raw = JSON.parse(receiptOutput.textContent); + } catch { + return; + } + if (raw.non_transfer && raw.provenance?.contract_blob && raw.receipt_id?.includes('sha256:')) { + reconciledRevision = inputRevision; + return; + } + reconciling = true; + try { + const receipt = await reconcileReceipt(raw); + receiptOutput.textContent = JSON.stringify(receipt, null, 2); + updatePairOutputs(receipt); + reconcileVector(); + reconciledRevision = inputRevision; + lab.dataset.strictReceiptReady = 'true'; + } finally { + reconciling = false; + } +} + +function downloadStrictReceipt(receiptText) { + const href = URL.createObjectURL(new Blob([receiptText], { type: 'application/json;charset=utf-8' })); + const link = document.createElement('a'); + link.href = href; + link.download = `gonol-relationship-${activeArity()}-strict-receipt.json`; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(href); +} + +async function waitForCurrentReceipt() { + const deadline = performance.now() + 2000; + while (performance.now() < deadline) { + await reconcileVisibleReceipt(); + if (reconciledRevision === inputRevision && lab.dataset.strictReceiptReady === 'true') return receiptOutput.textContent; + await new Promise(resolve => setTimeout(resolve, 40)); + } + throw new Error('The current strict receipt did not settle before download.'); +} + +lab.addEventListener('input', () => { + inputRevision += 1; + lab.dataset.strictReceiptReady = 'false'; +}, { capture: true }); +lab.addEventListener('change', () => { + inputRevision += 1; + lab.dataset.strictReceiptReady = 'false'; +}, { capture: true }); + +new MutationObserver(() => { + queueMicrotask(() => { + reconcileVisibleReceipt(); + reconcileVector(); + }); +}).observe(receiptOutput, { childList: true, characterData: true, subtree: true }); + +if (comparisonControl) { + const exactOption = comparisonControl.querySelector('option[value="exact-utf8"]'); + if (exactOption) exactOption.textContent = 'Browser-normalized LF UTF-8 bytes — experiment candidate'; +} +const operandIntro = lab.querySelector('#operands-heading + .section-intro'); +if (operandIntro) { + operandIntro.textContent = 'Enter source text or a serialized gonol. Unicode scalar values and case are preserved. HTML textarea values normalize CRLF and CR line endings to LF before hashing; every portable receipt declares that loss.'; +} + +downloadButton.addEventListener('click', async event => { + event.preventDefault(); + event.stopImmediatePropagation(); + statusOutput.textContent = 'Finalizing the current strict receipt…'; + try { + const text = await waitForCurrentReceipt(); + downloadStrictReceipt(text); + statusOutput.textContent = 'Current strict relationship receipt downloaded.'; + } catch { + statusOutput.textContent = 'Receipt download stopped because the current strict state could not be finalized.'; + } +}, { capture: true }); + +reconcileVisibleReceipt(); +reconcileVector(); diff --git a/src/assets/js/site.js b/src/assets/js/site.js index 4fd07b2..1444a21 100644 --- a/src/assets/js/site.js +++ b/src/assets/js/site.js @@ -1,8 +1,8 @@ // === MODULE_BUILD === // id: optional_site_enhancement -// purpose: Add compact navigation and export actions for each substantive text field without hiding static content. +// purpose: Add compact navigation and structure-preserving export actions for each substantive text field without hiding static content. // entrypoint: loaded with defer from the base layout -// tests: tests/site-contract.test.mjs +// tests: tests/site-contract.test.mjs, tests/post-merge-reconciliation.test.mjs // === END MODULE_BUILD === document.documentElement.classList.add('js'); @@ -18,11 +18,6 @@ if (button && nav) { }); } -// A public field is a deliberate, self-contained reading unit: not a heading, -// navigation link, or decorative label. Keep this list aligned with the site -// component vocabulary so new content using an established field class gains a -// copy action automatically. `data-copy-field` is the escape hatch for a new -// field type that has not yet earned a shared class. const COPYABLE_FIELD_SELECTOR = [ '[data-copy-field]', '#content .hero', @@ -44,15 +39,31 @@ const COPYABLE_FIELD_SELECTOR = [ '.site-footer .hmmm-boundary' ].join(', '); -function textForCopy(field) { +const TITLE_SELECTOR = 'h1, h2, h3, .m-title, .ref-title, summary, strong'; +const EXPORT_IGNORE_SELECTOR = '.field-actions, .copy-status, script, style, template, [data-copy-ignore]'; + +function cleanClone(field) { const copy = field.cloneNode(true); - copy.querySelectorAll('.field-actions, .copy-status, script, style, template, [data-copy-ignore]').forEach(node => node.remove()); - return copy.innerText.replace(/\n{3,}/g, '\n\n').trim(); + copy.querySelectorAll(EXPORT_IGNORE_SELECTOR).forEach(node => node.remove()); + return copy; +} + +function textForCopy(field) { + return cleanClone(field).innerText.replace(/\n{3,}/g, '\n\n').trim(); } function fieldTitle(field) { - const heading = field.querySelector('h1, h2, h3, summary, strong'); - return heading?.textContent?.replace(/\s+/g, ' ').trim() || document.title.replace(/\s+·\s+.*$/, '').trim() || 'field-text'; + const heading = field.querySelector(TITLE_SELECTOR); + return heading?.textContent?.replace(/\s+/g, ' ').trim() + || document.title.replace(/\s+·\s+.*$/, '').trim() + || 'field-text'; +} + +function textWithoutRepeatedTitle(field) { + const copy = cleanClone(field); + const heading = copy.querySelector(TITLE_SELECTOR); + if (heading && heading.textContent.replace(/\s+/g, ' ').trim() === fieldTitle(field)) heading.remove(); + return copy.innerText.replace(/\n{3,}/g, '\n\n').trim(); } function fieldLabel(field, action) { @@ -71,12 +82,69 @@ function fieldFilename(field, extension) { return `${filename}.${extension}`; } +function markdownEscape(value) { + return value.replace(/([\\`*_[\]<>])/g, '\\$1'); +} + +function inlineMarkdown(node) { + if (node.nodeType === Node.TEXT_NODE) return markdownEscape(node.textContent || ''); + if (node.nodeType !== Node.ELEMENT_NODE) return ''; + const tag = node.tagName.toLowerCase(); + const content = Array.from(node.childNodes).map(inlineMarkdown).join(''); + if (tag === 'a') { + const href = node.getAttribute('href'); + return href ? `[${content.trim() || href}](${new URL(href, document.baseURI).href})` : content; + } + if (tag === 'code') return `\`${(node.textContent || '').replace(/`/g, '\\`')}\``; + if (tag === 'strong' || tag === 'b') return `**${content}**`; + if (tag === 'em' || tag === 'i') return `*${content}*`; + if (tag === 'br') return ' \n'; + return content; +} + +function blockMarkdown(node, depth = 0) { + if (node.nodeType === Node.TEXT_NODE) return node.textContent?.trim() ? `${markdownEscape(node.textContent.trim())}\n\n` : ''; + if (node.nodeType !== Node.ELEMENT_NODE) return ''; + if (node.matches(EXPORT_IGNORE_SELECTOR)) return ''; + const tag = node.tagName.toLowerCase(); + const headingLevel = /^h([1-6])$/.exec(tag); + if (headingLevel) return `${'#'.repeat(Number(headingLevel[1]))} ${inlineMarkdown(node).trim()}\n\n`; + if (node.matches('.m-title, .ref-title')) return `### ${inlineMarkdown(node).trim()}\n\n`; + if (tag === 'a') { + const href = node.getAttribute('href'); + const body = Array.from(node.childNodes).map(child => blockMarkdown(child, depth)).join('').trim(); + if (!href) return body ? `${body}\n\n` : ''; + const title = node.querySelector(TITLE_SELECTOR)?.textContent?.replace(/\s+/g, ' ').trim() || href; + return `${body}\n\n[Open ${markdownEscape(title)}](${new URL(href, document.baseURI).href})\n\n`; + } + if (tag === 'p') return `${inlineMarkdown(node).trim()}\n\n`; + if (tag === 'pre') return `\`\`\`\n${node.textContent || ''}\n\`\`\`\n\n`; + if (tag === 'blockquote') return `${(node.innerText || '').split('\n').map(line => `> ${line}`).join('\n')}\n\n`; + if (tag === 'ul' || tag === 'ol') { + return `${Array.from(node.children).map((item, index) => { + const marker = tag === 'ol' ? `${index + 1}.` : '-'; + return `${' '.repeat(depth)}${marker} ${inlineMarkdown(item).trim()}`; + }).join('\n')}\n\n`; + } + if (tag === 'dl') { + return `${Array.from(node.children).map(child => child.tagName === 'DT' + ? `**${inlineMarkdown(child).trim()}**` + : `${inlineMarkdown(child).trim()}\n`).join('\n')}\n`; + } + if (tag === 'hr') return '---\n\n'; + return Array.from(node.childNodes).map(child => blockMarkdown(child, depth + 1)).join(''); +} + +function markdownForField(field) { + const copy = cleanClone(field); + return blockMarkdown(copy).replace(/\n{3,}/g, '\n\n').trim(); +} + async function writeText(text) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } - const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); @@ -90,7 +158,7 @@ async function writeText(text) { function downloadMarkdown(field) { const link = document.createElement('a'); - link.href = URL.createObjectURL(new Blob([`${textForCopy(field)}\n`], { type: 'text/markdown;charset=utf-8' })); + link.href = URL.createObjectURL(new Blob([`${markdownForField(field)}\n`], { type: 'text/markdown;charset=utf-8' })); link.download = fieldFilename(field, 'md'); link.hidden = true; document.body.append(link); @@ -107,10 +175,10 @@ function printPdf(field) { const printWindow = window.open('', '_blank', 'popup'); if (!printWindow) return false; printWindow.opener = null; - const title = fieldTitle(field); + const body = textWithoutRepeatedTitle(field); printWindow.document.open(); - printWindow.document.write(`
${escapeHtml(textForCopy(field))}`);
+ printWindow.document.write(`${escapeHtml(body)}`);
printWindow.document.close();
printWindow.focus();
window.setTimeout(() => printWindow.print(), 100);
@@ -119,31 +187,16 @@ function printPdf(field) {
function addCopyControl(field) {
if (field.dataset.copyReady === 'true' || !textForCopy(field)) return;
-
const actions = document.createElement('div');
actions.className = 'field-actions';
actions.setAttribute('data-copy-ignore', '');
-
- const copyControl = document.createElement('button');
- copyControl.type = 'button';
- copyControl.className = 'copy-button';
- copyControl.textContent = 'Copy';
+ const copyControl = Object.assign(document.createElement('button'), { type: 'button', className: 'copy-button', textContent: 'Copy' });
copyControl.setAttribute('aria-label', fieldLabel(field, 'Copy'));
-
- const markdownControl = document.createElement('button');
- markdownControl.type = 'button';
- markdownControl.className = 'copy-button';
- markdownControl.textContent = '.md';
+ const markdownControl = Object.assign(document.createElement('button'), { type: 'button', className: 'copy-button', textContent: '.md' });
markdownControl.setAttribute('aria-label', fieldLabel(field, 'Download Markdown for'));
-
- const pdfControl = document.createElement('button');
- pdfControl.type = 'button';
- pdfControl.className = 'copy-button';
- pdfControl.textContent = 'PDF';
+ const pdfControl = Object.assign(document.createElement('button'), { type: 'button', className: 'copy-button', textContent: 'PDF' });
pdfControl.setAttribute('aria-label', fieldLabel(field, 'Print or save as PDF for'));
-
actions.append(copyControl, markdownControl, pdfControl);
-
const status = document.createElement('span');
status.className = 'copy-status';
status.setAttribute('role', 'status');
@@ -151,14 +204,12 @@ function addCopyControl(field) {
let controlField = field;
if (field.matches('a')) {
- // An anchor cannot contain an interactive descendant. Its copy action is
- // therefore a sibling in a small wrapper, while the copied text remains
- // exactly the card/link field.
const wrapper = document.createElement('div');
wrapper.className = 'copy-field copy-field-link';
field.replaceWith(wrapper);
wrapper.append(actions, status, field);
controlField = wrapper;
+ if (field.matches('.card')) field.style.paddingTop = '4.75rem';
} else if (field.matches('details')) {
field.append(actions, status);
} else {
@@ -187,3 +238,10 @@ function addCopyControl(field) {
}
document.querySelectorAll(COPYABLE_FIELD_SELECTOR).forEach(addCopyControl);
+
+if (document.querySelector('[data-gonol-lab]')) {
+ import('/assets/js/gonol-reconciliation.js').catch(() => {
+ const status = document.querySelector('[data-gonol-status]');
+ if (status) status.textContent = 'Strict receipt reconciliation could not load; do not treat generated receipts as portable evidence.';
+ });
+}
diff --git a/tests/gonol-relationships.test.mjs b/tests/gonol-relationships.test.mjs
index 907d29a..3c0d46e 100644
--- a/tests/gonol-relationships.test.mjs
+++ b/tests/gonol-relationships.test.mjs
@@ -30,8 +30,8 @@
// cleanup: none
//
// id: check_gonol_receipt_provenance_boundary
-// proves: gonol_receipt_retains_producer_boundary
-// call: self::page and validator provenance tests
+// proves: gonol_receipt_retains_producer_boundary, website_gonol_portable_receipt_is_strict
+// call: self::page, reconciliation, schema, and validator provenance tests
// mutates: none
// cleanup: none
//
@@ -48,10 +48,10 @@ import { readFile } from 'node:fs/promises';
import { validateGonolRelationshipPublication } from '../scripts/check-gonol-relationship-display-drift.mjs';
-test('commit-pinned producer contract and receipt schema remain exact', () => {
+test('commit-pinned producer contract and strict receipt schema remain exact', () => {
const result = validateGonolRelationshipPublication();
assert.equal(result.contractSha256, 'd1ac67d4a5d5b4cd0622056af94b24a1404f1f5c7d7dfeb59ee28e5edb73aad5');
- assert.equal(result.receiptSchemaSha256, 'a6565d92e233768fd306f21f2a40a0f38867b32c5740e26fd299c0e23af1d9bf');
+ assert.equal(result.receiptSchemaSha256, '2b15b1fbc517fdf11e617da8d31ae542cb6f240755ba12390a5bbc7d81034d56');
assert.deepEqual(result.primitiveArities, [1, 2, 3, 7]);
assert.equal(result.publicGonolArity, 157);
});
@@ -82,18 +82,20 @@ test('vesica and triquetra geometry retain every declared relationship piece', a
assert.equal(primitives.get(7).pairing_plan_required, true);
});
-test('artifact is static-first, copy-compatible, and keeps comparison explicit', async () => {
- const [page, script, config, packageJson, index] = await Promise.all([
+test('artifact is static-first and reconciliation makes portable receipts strict', async () => {
+ const [page, producerScript, reconciliation, config, packageJson, index, schema, publication] = await Promise.all([
readFile('src/artifacts/gonol-relationships.njk', 'utf8'),
readFile('src/assets/js/gonol-relationships.js', 'utf8'),
+ readFile('src/assets/js/gonol-reconciliation.js', 'utf8'),
readFile('.eleventy.js', 'utf8'),
readFile('package.json', 'utf8'),
- readFile('src/artifacts/index.njk', 'utf8')
+ readFile('src/artifacts/index.njk', 'utf8'),
+ readFile('src/assets/data/gonol-relationship-receipt-v1.schema.json', 'utf8'),
+ readFile('src/_data/gonol_relationship_publication.json', 'utf8')
]);
assert.match(page, /permalink: \/artifacts\/gonol-relationships\//);
assert.match(page, /Represent relationship only — no comparison/);
- assert.match(page, /Exact UTF-8 bytes — experiment candidate/);
assert.match(page, /data-receipt-output/);
assert.match(page, /source-block gonol-output/);
assert.match(page, /data-public-gonol-grid[^>]*tabindex="0"/);
@@ -101,12 +103,30 @@ test('artifact is static-first, copy-compatible, and keeps comparison explicit',
assert.match(page, /Seven identities may be retained; no geometry or pairing count is inferred/);
assert.match(index, /href="\/artifacts\/gonol-relationships\/"/);
- assert.doesNotMatch(script, /\bfetch\s*\(/);
- assert.doesNotMatch(script, /\.normalize\s*\(/);
- assert.match(script, /comparison_policy: policy/);
- assert.match(script, /no seven-form geometry or pairing count was inferred/);
- assert.match(script, /\['A', 'B'\], \['B', 'C'\], \['C', 'A'\]/);
- assert.match(script, /producer_authenticated: false/);
+ assert.doesNotMatch(producerScript, /\bfetch\s*\(/);
+ assert.doesNotMatch(producerScript, /\.normalize\s*\(/);
+ assert.match(producerScript, /\['A', 'B'\], \['B', 'C'\], \['C', 'A'\]/);
+
+ assert.match(reconciliation, /stopImmediatePropagation\(\)/);
+ assert.match(reconciliation, /html-textarea-value-crlf-and-cr-to-lf/);
+ assert.match(reconciliation, /contract_blob: publication\.source\.blob/);
+ assert.match(reconciliation, /derived_layers_visible/);
+ assert.match(reconciliation, /unicode_scalar_value/);
+ assert.match(reconciliation, /carrier_position_or_null/);
+ assert.match(reconciliation, /website-gonol-relation:sha256:/);
+ assert.match(reconciliation, /semantic_authority_transfer: false/);
+ assert.match(reconciliation, /producer_authentication_transfer: false/);
+
+ const parsedSchema = JSON.parse(schema);
+ assert.ok(parsedSchema.required.includes('candidate_identity'));
+ assert.ok(parsedSchema.required.includes('non_transfer'));
+ assert.ok(parsedSchema.properties.provenance.required.includes('contract_blob'));
+ assert.equal(parsedSchema.$defs.operand.properties.native_scale.properties.numeric_rank.const, null);
+ assert.ok(parsedSchema.$defs.displayPolicy.properties.parameters.required.includes('derived_layers_visible'));
+
+ const parsedPublication = JSON.parse(publication);
+ assert.equal(parsedPublication.publication_status, 'merged-producer-reconciled-consumer');
+ assert.equal(parsedPublication.receipt_schema.blob, '569ff5ce8a891a879314f2845905b7c9b8bc085c');
assert.match(config, /gonol_relationship_display\.json.*gonol-relationship-display-v1\.json/);
assert.match(packageJson, /check:gonol-relationships/);
diff --git a/tests/post-merge-reconciliation.test.mjs b/tests/post-merge-reconciliation.test.mjs
new file mode 100644
index 0000000..f0025de
--- /dev/null
+++ b/tests/post-merge-reconciliation.test.mjs
@@ -0,0 +1,60 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { createHash } from 'node:crypto';
+import { readFile } from 'node:fs/promises';
+import { spawnSync } from 'node:child_process';
+
+const SITE_SCRIPT = 'src/assets/js/site.js';
+const RECONCILIATION_SCRIPT = 'src/assets/js/gonol-reconciliation.js';
+const STRICT_SCHEMA = 'src/assets/data/gonol-relationship-receipt-v1.schema.json';
+
+function syntaxCheck(path) {
+ const result = spawnSync(process.execPath, ['--check', path], { encoding: 'utf8' });
+ assert.equal(result.status, 0, `${path} syntax failure:\n${result.stderr || result.stdout}`);
+}
+
+test('site-wide export controls preserve structure, root destinations, and distinct titles', async () => {
+ const source = await readFile(SITE_SCRIPT, 'utf8');
+ syntaxCheck(SITE_SCRIPT);
+
+ assert.match(source, /\.m-title, \.ref-title/);
+ assert.match(source, /node\.matches\('\.m-title, \.ref-title'\)/);
+ assert.match(source, /function markdownForField\(field\)/);
+ assert.match(source, /if \(tag === 'a'\)/);
+ assert.match(source, /\[Open \$\{markdownEscape\(title\)\}\]/);
+ assert.match(source, /new URL\(href, document\.baseURI\)\.href/);
+ assert.match(source, /function textWithoutRepeatedTitle\(field\)/);
+ assert.match(source, /heading\.remove\(\)/);
+ assert.match(source, /field\.style\.paddingTop = '4\.75rem'/);
+ assert.match(source, /import\('\/assets\/js\/gonol-reconciliation\.js'\)/);
+});
+
+test('Gonol reconciliation fails closed and emits strict portable identity', async () => {
+ const source = await readFile(RECONCILIATION_SCRIPT, 'utf8');
+ syntaxCheck(RECONCILIATION_SCRIPT);
+
+ assert.match(source, /event\.stopImmediatePropagation\(\)/);
+ assert.match(source, /performance\.now\(\) \+ 2000/);
+ assert.match(source, /html-textarea-value-crlf-and-cr-to-lf/);
+ assert.match(source, /contract_blob: publication\.source\.blob/);
+ assert.match(source, /derived_layers_visible: Boolean/);
+ assert.match(source, /unicode_scalar_value/);
+ assert.match(source, /carrier_position_or_null/);
+ assert.match(source, /delete identity\.receipt_id/);
+ assert.match(source, /website-gonol-relation:sha256:/);
+ assert.match(source, /producer_authentication_transfer: false/);
+});
+
+test('website carries the exact merged strict UCNS schema bytes', async () => {
+ const bytes = await readFile(STRICT_SCHEMA);
+ const digest = createHash('sha256').update(bytes).digest('hex');
+ const schema = JSON.parse(bytes.toString('utf8'));
+
+ assert.equal(digest, '2b15b1fbc517fdf11e617da8d31ae542cb6f240755ba12390a5bbc7d81034d56');
+ assert.ok(schema.required.includes('candidate_identity'));
+ assert.ok(schema.required.includes('non_transfer'));
+ assert.ok(schema.properties.provenance.required.includes('contract_blob'));
+ assert.equal(schema.$defs.operand.properties.native_scale.properties.numeric_rank.const, null);
+ assert.deepEqual(schema.$defs.pairAB.allOf[1].properties.pair_id, { const: 'A-B' });
+ assert.ok(schema.$defs.displayPolicy.properties.parameters.required.includes('derived_layers_visible'));
+});