From 3f875ae96b579e7112f8629c04af3feedeb9a863 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 03:52:52 -0700 Subject: [PATCH 1/9] feat: establish ReplaceRange Edict application boundary --- .gitignore | 3 + CHANGELOG.md | 4 + edict/replace-range/README.md | 42 ++++ edict/replace-range/edict.application.json | 20 ++ edict/replace-range/edict.lawpack.json | 207 ++++++++++++++++++ edict/replace-range/src/ReplaceRange.edict | 41 ++++ edict/replace-range/tests/build.sh | 52 +++++ .../vendor/jedit-text/adapter.cbor | Bin 0 -> 431 bytes .../vendor/jedit-text/adapter.sha256 | 1 + .../echo-operation-configuration.cbor | 1 + .../echo-operation-configuration.sha256 | 1 + .../jedit-text/edict.lawpack-output.json | 67 ++++++ .../vendor/jedit-text/exports.cbor | Bin 0 -> 2212 bytes .../vendor/jedit-text/exports.sha256 | 1 + .../vendor/jedit-text/manifest.cbor | Bin 0 -> 787 bytes .../vendor/jedit-text/manifest.sha256 | 1 + .../vendor/jedit-text/verifier-rules.cbor | 1 + .../vendor/jedit-text/verifier-rules.sha256 | 1 + 18 files changed, 443 insertions(+) create mode 100644 edict/replace-range/README.md create mode 100644 edict/replace-range/edict.application.json create mode 100644 edict/replace-range/edict.lawpack.json create mode 100644 edict/replace-range/src/ReplaceRange.edict create mode 100755 edict/replace-range/tests/build.sh create mode 100644 edict/replace-range/vendor/jedit-text/adapter.cbor create mode 100644 edict/replace-range/vendor/jedit-text/adapter.sha256 create mode 100644 edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor create mode 100644 edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 create mode 100644 edict/replace-range/vendor/jedit-text/edict.lawpack-output.json create mode 100644 edict/replace-range/vendor/jedit-text/exports.cbor create mode 100644 edict/replace-range/vendor/jedit-text/exports.sha256 create mode 100644 edict/replace-range/vendor/jedit-text/manifest.cbor create mode 100644 edict/replace-range/vendor/jedit-text/manifest.sha256 create mode 100644 edict/replace-range/vendor/jedit-text/verifier-rules.cbor create mode 100644 edict/replace-range/vendor/jedit-text/verifier-rules.sha256 diff --git a/.gitignore b/.gitignore index 733790b..ff1f05b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ title-recordings/ .wesley-cache/ .jedit-cache/ .jedit/ +.build/ +.vendor.edict-lawpack-build.lock +.jedit-text.edict-lawpack-build.lock .obsidian/ .claude/ src/generated/jedit/structural-history-replace-text-range.wesley.generated.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e968747..9680ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Added the Jedit-owned `ReplaceRange.edict` application root and canonical + `jedit.text@1` lawpack publication. Its executable integration gate now proves + that real source reaches Edict's public Target IR boundary and fails closed + before package emission while generic Core `let` lowering remains unsupported. - Made `ReplaceRange` corroborate the selected Head/root byte extent and exact requested-range materialization before no-op classification, so truncated or absent retained roots fail with a typed malformed-rope obstruction. diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md new file mode 100644 index 0000000..4c03019 --- /dev/null +++ b/edict/replace-range/README.md @@ -0,0 +1,42 @@ +# ReplaceRange Edict application + +This directory is the Jedit-owned application root for issue #296. It keeps +five artifact classes separate: + +- `edict.lawpack.json` is the reviewable `jedit.text@1` authoring input; +- `vendor/jedit-text/` is Edict's canonical published lawpack closure; +- `src/ReplaceRange.edict` is the application-owned source; +- `.build/` contains disposable provider and compiler outputs; +- `contracts/jedit/lawpacks/replace-range-v1/` remains the independent schema + and oracle corpus and is not executable input. + +## Current executable boundary + +The checked-in source is the first compiler-pressure slice, not a completed +text mutation. It binds bounded identity bytes, the selected basis, range, +replacement bytes, operation profile, budget, imported helper implementation, +and a pure conditional into Edict Core. It intentionally does not claim to +traverse or rewrite a rope yet. + +The public application build currently reaches Edict Target IR lowering and +fails closed because the selected generic target profile cannot lower the Core +`let` nodes emitted for the digest-bound helper and conditional. No package is +emitted and Echo is not invoked. That failure is the routing evidence required +by #296: the next owner is Edict's generic Core-to-Target-IR boundary, not Echo +and not a native Jedit planner. + +## Reproduce + +The build script requires exact local Edict and Echo checkouts: + +```bash +EDICT_REPO=/path/to/edict \ +ECHO_REPO=/path/to/echo \ + ./edict/replace-range/tests/build.sh +``` + +The script first republishes `jedit.text@1` through Edict's public lawpack +authoring boundary, then copies Echo's checked provider package into the +disposable application build tree and invokes Edict's public application build. +Until the generic lowering gap is implemented, it requires the stable +`TargetLoweringFailed` refusal and rejects accidental package emission. diff --git a/edict/replace-range/edict.application.json b/edict/replace-range/edict.application.json new file mode 100644 index 0000000..f3e59f8 --- /dev/null +++ b/edict/replace-range/edict.application.json @@ -0,0 +1,20 @@ +{ + "schema": "edict.application/v1", + "coordinate": "jedit.text.replace_range@1", + "sources": [ + "src/ReplaceRange.edict" + ], + "lawpacks": [ + { + "manifest": "vendor/jedit-text/manifest.cbor", + "exports": "vendor/jedit-text/exports.cbor", + "adapter": "vendor/jedit-text/adapter.cbor", + "targetConfiguration": "vendor/jedit-text/echo-operation-configuration.cbor" + } + ], + "target": { + "profile": "echo.dpo@1", + "providerPackage": ".build/echo-provider" + }, + "outputDirectory": ".build/application" +} diff --git a/edict/replace-range/edict.lawpack.json b/edict/replace-range/edict.lawpack.json new file mode 100644 index 0000000..81865aa --- /dev/null +++ b/edict/replace-range/edict.lawpack.json @@ -0,0 +1,207 @@ +{ + "schema": "edict.lawpack-build/v1", + "outputDirectory": "vendor/jedit-text", + "lawpack": { + "schema": "edict.lawpack-authoring/v1", + "id": "jedit.text", + "version": "1", + "acceptedCoreAbi": [ + "edict.core/v1" + ], + "dependencies": [], + "exportsCoordinate": "jedit.text.exports/v1", + "exports": { + "types": [ + { + "coordinate": "jedit.text@1.NodeId", + "definition": "Bytes" + }, + { + "coordinate": "jedit.text@1.ReplacementBytes", + "definition": "Bytes" + }, + { + "coordinate": "jedit.text@1.ObstructionDetail", + "definition": "String" + } + ], + "constants": [ + { + "coordinate": "jedit.text@1.maxCreatedLeaves", + "type": "U64", + "value": 4096 + }, + { + "coordinate": "jedit.text@1.maxCreatedBranches", + "type": "U64", + "value": 4096 + } + ], + "pureFunctions": [ + { + "source": "edict", + "coordinate": "jedit.text@1.maxCreatedLeafCount", + "typeParameters": [], + "parameterTypes": [], + "returnType": "U64", + "costTemplate": "jedit.text@1.replaceRangeBudget", + "determinismClass": "total", + "body": { + "params": [], + "body": { + "locals": [], + "bindings": [], + "result": { + "kind": "const", + "value": { + "kind": "int", + "width": "U64", + "value": 4096 + } + } + } + } + } + ], + "effects": [], + "obstructions": [ + { + "coordinate": "jedit.text@1.RangeOrderInvalid", + "authorityClass": "domainMappable", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.RangeOutOfBounds", + "authorityClass": "domainMappable", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.Utf8BoundaryInvalid", + "authorityClass": "domainMappable", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.NoOp", + "authorityClass": "domainMappable", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.BasisNotCanonical", + "authorityClass": "domainMappable", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.ArithmeticOverflow", + "authorityClass": "integrityFault", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.FactMissing", + "authorityClass": "resourceFault", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.FactMalformed", + "authorityClass": "integrityFault", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.ContentIdentityMismatch", + "authorityClass": "integrityFault", + "payloadSchema": "jedit.text@1.ObstructionDetail" + }, + { + "coordinate": "jedit.text@1.MalformedRope", + "authorityClass": "integrityFault", + "payloadSchema": "jedit.text@1.ObstructionDetail" + } + ], + "operationProfiles": { + "jedit.text@1.replaceRange": { + "opticTemplate": { + "opticKind": "affectReintegration", + "boundaryKind": "affect", + "supportPolicy": "jedit.text@1.basisBoundSupport", + "lossDisposition": "jedit.text@1.lossless", + "basisTemplate": "jedit.text@1.exactHeadBasis", + "apertureRequirement": { + "kind": "abstractFootprintObligation", + "reference": "jedit.text@1.replaceRangeFootprint" + } + }, + "effectPredicate": "jedit.text@1.replaceRangeEffects" + } + } + }, + "targetAdapters": [ + { + "coordinate": "jedit.text.echo-adapter/v1", + "output": "adapter.cbor", + "acceptedTargetProfile": { + "id": "echo.dpo@1", + "digest": "sha256:2e2494121aecf5e6a2d920f5fb85408825d394765fad41484c416397c920fb04" + }, + "acceptedTargetIr": { + "id": "echo.span-ir/v1", + "digest": "sha256:0057167e68f50c99dcce087b3e1cd677d17c5d1dc238bdb52d89469e1472fc2f" + }, + "operationProfiles": { + "jedit.text@1.replaceRange": { + "core": "continuum.profile.read-write/v1", + "semanticEffects": [], + "budgetObligation": "jedit.text@1.replaceRangeBudget", + "targetConfiguration": { + "local": "echo-config" + } + } + }, + "effectImplementations": {}, + "budgets": { + "jedit.text@1.replaceRangeBudget": { + "maxSteps": 1048576, + "maxAllocatedBytes": 16777216, + "maxOutputBytes": 8388608 + } + } + } + ], + "verifier": { + "class": "declarative", + "ruleset": { + "local": "verifier-rules" + } + }, + "compatibility": { + "id": "jedit.text.compatibility/v1", + "digest": "sha256:2adf83f798205f52636ad64ca45178f33a8e1cc663874f8a6113484c945e3052" + }, + "conformanceFixtureCorpus": { + "id": "jedit.text.ReplaceRange.oracle@1", + "digest": "sha256:b9b4707dd5e07897cbe7af36d88928401887e6af5d167d1a88a8d6181b4dbb0e" + }, + "localResources": [ + { + "name": "echo-config", + "coordinate": "jedit.text.echo-config/v1", + "output": "echo-operation-configuration.cbor", + "value": { + "apiVersion": "jedit.text.echo-config/v1", + "instructionProfile": "echo.dpo@1.generic-bounded-graph", + "operation": "jedit.text@1.replaceRange" + } + }, + { + "name": "verifier-rules", + "coordinate": "jedit.text.verifier-rules/v1", + "output": "verifier-rules.cbor", + "value": { + "schema": "jedit.text.schema@1", + "schemaDigest": "sha256:f49b0ff6c9c7010d3a72b46734d47832853f496739a083edc41dddec3431bac7", + "oracleIsExecutableInput": false, + "runtimeOntology": "generic-bounded-graph" + } + } + ] + }, + "dependencyBundles": [] +} diff --git a/edict/replace-range/src/ReplaceRange.edict b/edict/replace-range/src/ReplaceRange.edict new file mode 100644 index 0000000..cd317ee --- /dev/null +++ b/edict/replace-range/src/ReplaceRange.edict @@ -0,0 +1,41 @@ +package jedit.text.replace_range@1; + +use lawpack jedit.text@1 digest "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" as text; + +type ReplaceRangeInput = { + bufferId: Bytes, + basisHeadId: Bytes, + startByte: U64, + endByte: U64, + replacement: Bytes, +}; + +type ReplaceRangeBoundary = { + bufferId: Bytes, + basisHeadId: Bytes, + startByte: U64, + endByte: U64, + replacement: Bytes, + rangeIsEmpty: U32, + createdLeafCeiling: U64, +}; + +intent replaceRange(input: ReplaceRangeInput) returns ReplaceRangeBoundary + profile text.replaceRange + basis input.basisHeadId + budget <= text.replaceRangeBudget + where input.startByte <= input.endByte +{ + let createdLeafCeiling: U64 = text.maxCreatedLeafCount(); + let rangeIsEmpty = if input.startByte == input.endByte then 1u32 else 0u32; + + return { + bufferId: input.bufferId, + basisHeadId: input.basisHeadId, + startByte: input.startByte, + endByte: input.endByte, + replacement: input.replacement, + rangeIsEmpty, + createdLeafCeiling, + }; +} diff --git a/edict/replace-range/tests/build.sh b/edict/replace-range/tests/build.sh new file mode 100755 index 0000000..27f75ee --- /dev/null +++ b/edict/replace-range/tests/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) +application_root="$project_root/edict/replace-range" +edict_repo=${EDICT_REPO:?set EDICT_REPO to an exact Edict checkout} +echo_repo=${ECHO_REPO:?set ECHO_REPO to an exact Echo checkout} +edict_bin=${EDICT_BIN:-"$edict_repo/target/debug/edict"} +provider_source="$echo_repo/schemas/edict-provider/package/v1" + +test -x "$edict_bin" +test -f "$provider_source/provider-manifest.echo.json" +test -f "$provider_source/components/lowerer.echo-dpo.component.wasm" +test -f "$provider_source/components/verifier.echo-dpo.component.wasm" + +if find "$provider_source" -type l -print -quit | grep -q .; then + echo "provider package must not contain symlinks" >&2 + exit 1 +fi + +cd "$application_root" + +printf '%s\n' \ + '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","lawpack":"edict.lawpack.json"}' \ + | "$edict_bin" + +rm -rf .build/echo-provider .build/application +mkdir -p .build/echo-provider +cp -RL "$provider_source/." .build/echo-provider/ + +if find .build/echo-provider -type l -print -quit | grep -q .; then + echo "copied provider package must not contain symlinks" >&2 + exit 1 +fi + +set +e +build_output=$( + printf '%s\n' \ + '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","application":"edict.application.json"}' \ + | "$edict_bin" 2>&1 +) +build_status=$? +set -e + +printf '%s\n' "$build_output" +test "$build_status" -ne 0 +printf '%s\n' "$build_output" \ + | jq --exit-status --slurp \ + 'any(.[]; .type == "diagnostic" and .kind == "TargetLoweringFailed")' \ + >/dev/null +test ! -e .build/application/executable-operation-package.cbor +test ! -e .build/application/verification-report.cbor diff --git a/edict/replace-range/vendor/jedit-text/adapter.cbor b/edict/replace-range/vendor/jedit-text/adapter.cbor new file mode 100644 index 0000000000000000000000000000000000000000..c96488df0963dbf0b2283b73bd06eb953530e455 GIT binary patch literal 431 zcmaiwp-uxq5QZ0pMM5iz!1hQS&X zz#u>&afIMV9ggjt!4TAB{`@olH*=3%f%knes}4vGG3jI?LGm|wt%pKVM3~c7BXVdJ za6AB=V6(8?c2mgPgw|Ks0%JOKt&D6&%@e|)jTsvRZCfeBfl!q49Am5pRvx1B(&&6K zgm8)!j4bee9RyfHw>oSLjKvv}(fiKCQei+^`V;9+aN{tqaidAP6cof^^uc= zVX5($LU8ciI7Uu>Y|C?79VL?@?BBCJfeR1XF*=q>RuWD+Qv8OCkatZzumnp;jfG6m z(|PQ>u(rN&v|Rc4Jo|o-7su7d=da85>FeZbe)_aB|I&)5@72w>3=6!y)GAC-Q$gmh Fe*pW=s=)vN literal 0 HcmV?d00001 diff --git a/edict/replace-range/vendor/jedit-text/adapter.sha256 b/edict/replace-range/vendor/jedit-text/adapter.sha256 new file mode 100644 index 0000000..152492e --- /dev/null +++ b/edict/replace-range/vendor/jedit-text/adapter.sha256 @@ -0,0 +1 @@ +sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828 diff --git a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor new file mode 100644 index 0000000..b2057f9 --- /dev/null +++ b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor @@ -0,0 +1 @@ +£ioperationxjedit.text@1.replaceRangejapiVersionxjedit.text.echo-config/v1rinstructionProfilex echo.dpo@1.generic-bounded-graph \ No newline at end of file diff --git a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 new file mode 100644 index 0000000..2612bf6 --- /dev/null +++ b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 @@ -0,0 +1 @@ +sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de diff --git a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json new file mode 100644 index 0000000..4f76b71 --- /dev/null +++ b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json @@ -0,0 +1,67 @@ +{ + "schema": "edict.lawpack-output/v1", + "lawpackId": "jedit.text", + "lawpackVersion": "1", + "artifacts": [ + { + "path": "manifest.cbor", + "kind": "manifest", + "coordinate": "edict.lawpack/v1", + "digest": "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" + }, + { + "path": "manifest.sha256", + "kind": "manifestDigest", + "coordinate": "edict.lawpack/v1", + "digest": "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" + }, + { + "path": "exports.cbor", + "kind": "exports", + "coordinate": "jedit.text.exports/v1", + "digest": "sha256:2a4ef08bec9f2839aa69841a6458eabc037f1ea698fd1a68392aeb84d5b306be" + }, + { + "path": "exports.sha256", + "kind": "exportsDigest", + "coordinate": "jedit.text.exports/v1", + "digest": "sha256:2a4ef08bec9f2839aa69841a6458eabc037f1ea698fd1a68392aeb84d5b306be" + }, + { + "path": "echo-operation-configuration.cbor", + "kind": "localResource", + "coordinate": "jedit.text.echo-config/v1", + "digest": "sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de" + }, + { + "path": "echo-operation-configuration.sha256", + "kind": "localResourceDigest", + "coordinate": "jedit.text.echo-config/v1", + "digest": "sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de" + }, + { + "path": "verifier-rules.cbor", + "kind": "localResource", + "coordinate": "jedit.text.verifier-rules/v1", + "digest": "sha256:364fb4ff98ae64fcd8d793aae3d7cb133ff8a1ce661330cea47688b13a0a823f" + }, + { + "path": "verifier-rules.sha256", + "kind": "localResourceDigest", + "coordinate": "jedit.text.verifier-rules/v1", + "digest": "sha256:364fb4ff98ae64fcd8d793aae3d7cb133ff8a1ce661330cea47688b13a0a823f" + }, + { + "path": "adapter.cbor", + "kind": "adapter", + "coordinate": "jedit.text.echo-adapter/v1", + "digest": "sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828" + }, + { + "path": "adapter.sha256", + "kind": "adapterDigest", + "coordinate": "jedit.text.echo-adapter/v1", + "digest": "sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828" + } + ] +} diff --git a/edict/replace-range/vendor/jedit-text/exports.cbor b/edict/replace-range/vendor/jedit-text/exports.cbor new file mode 100644 index 0000000000000000000000000000000000000000..ab18a6b613d15e096bb3f507ba661ab3fe82ed6a GIT binary patch literal 2212 zcmcIlO>Yx15alOuq*Qzeaju}Wr3a`WDG?R4iPH85IQDGf;I+4&*>-aaaN}Q)NEJVm zv6ql_E0sgYWmnmF=FOWKzjqBZDFDCE#?m>jv=sz62Bm3>prUs#w@#dbBQ;i#X{$*) zo3|!>=1nfDx39Jjnt|1$B(e{M5fXB+6hj(0{4&SO7d!jAulLeKkCQ~*0g^9e06zp0 z+B7n`Q}WslV>#Qqdj~_vGLVE*EuBRYmhjtL1(Nc7ZwD@gDPiOOy$0PIRWq-)2X2G< z4B`^-AHh$ON|(VcFRUB^8n-MSf6SY>DapRDL+E-1{>XBbTIGe97$?*zr{TQm>02$a zqNI`YnkH={5Up~#(Dqmqg%}t}aBoa0;+E9STCTJT*9QAE>C`!8`w>w1$(jjYB)})G zTda*^I;56BjVF%U!aA#^Fl)n2x9~l$!6*mPvU>?WGwy0NU~LJ8VYNDz!i${W^>l%KMp9D}Q;k@vDNAwIW_hBq2P(Zi`=jSZI;1o1 zgJHq0(p%tb`-!^RD%B9kMtC6bAs(kT*g`Lq6u#lKP~n}k&tV=_6v&Y<7u;ztg8THm z%d`RfS#2aM(S(}iuHc)uDC%0rpP#q|D55p>p&oY3Xa+%aSp&m?tUizrf^IAlRGSGD zRD8!if>2Rhax9C&dFnZ%<>ZD(#57T3PwP|8IK)GZg~RCNWs*7k$=nbH)8!^ZA1+Gm zqkqq&hpc**oNtouI7bEhFvUHnHbpQ!F7)sB?!f3_WbA5#?)fnMFtv2%&8#14EB_5s CA8PUd literal 0 HcmV?d00001 diff --git a/edict/replace-range/vendor/jedit-text/exports.sha256 b/edict/replace-range/vendor/jedit-text/exports.sha256 new file mode 100644 index 0000000..41aa6fa --- /dev/null +++ b/edict/replace-range/vendor/jedit-text/exports.sha256 @@ -0,0 +1 @@ +sha256:2a4ef08bec9f2839aa69841a6458eabc037f1ea698fd1a68392aeb84d5b306be diff --git a/edict/replace-range/vendor/jedit-text/manifest.cbor b/edict/replace-range/vendor/jedit-text/manifest.cbor new file mode 100644 index 0000000000000000000000000000000000000000..dac00a5ab49fa14f63804e195860ade1c1416bea GIT binary patch literal 787 zcmZ3Ll$ny1m70=SqF0hyQIeioQIKC$QoIN#T#69Zg9_`H8K$LVrl%H{G^G`1BpR8T zMJQWK*{FTbE)Oy`=bu-((^s>~V;>`TKM8gaqD>E%K zwP;ala!z7#adrxbE=nxPEK5x)2?;)nuQQSdbY8aYF&nqsb+DIf>;3iOJbO!*Wtm3sUn^ zQuC5CQ;QpNlk;;6fX+$E%*iaN1Up3<$tftZ*j%D@zxn$Nh4`T4tZP0?0xLdS^~oGd zZujp>6!!3$5@!&USCUwio?7CVl2}jzbWr2s^h5{~Y?Ty}RjJ7t`MOX^Y&LoMt<5&P zns`um)}CwL^K!mQJb7npYhJK!x@csI{ss*Vp!bqfftI9%fNk_F0@;ucGF7j*ATduj zvk0514B=vR8DDv3-Z{rnZ6|ZB{9;Y4>>-Q2TXj3#=7|*j(Jw_cHJ~UzEi)$-WONqT z=#+wd2h0Sbr!qxI>dn_@i*71>{oU%&p?Y~rS^Qc@4&Wraj?eqLIBQEp;ha;jTqMM-H Date: Sun, 23 Aug 2026 04:27:26 -0700 Subject: [PATCH 2/9] test: advance ReplaceRange compiler boundary --- CHANGELOG.md | 8 +++++--- edict/replace-range/README.md | 24 ++++++++++++++++-------- edict/replace-range/tests/build.sh | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9680ee4..bc7578c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## Unreleased - Added the Jedit-owned `ReplaceRange.edict` application root and canonical - `jedit.text@1` lawpack publication. Its executable integration gate now proves - that real source reaches Edict's public Target IR boundary and fails closed - before package emission while generic Core `let` lowering remains unsupported. + `jedit.text@1` lawpack publication. With Edict #201, its executable integration + gate now proves that real source lowers into generic pure Target IR and reaches + Echo's provider-contract boundary, which refuses the new artifact family + before package emission. Rope mutation and end-to-end Jim execution remain + unimplemented. - Made `ReplaceRange` corroborate the selected Head/root byte extent and exact requested-range materialization before no-op classification, so truncated or absent retained roots fail with a typed malformed-rope obstruction. diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index 4c03019..e7300ad 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -18,12 +18,18 @@ replacement bytes, operation profile, budget, imported helper implementation, and a pure conditional into Edict Core. It intentionally does not claim to traverse or rewrite a rope yet. -The public application build currently reaches Edict Target IR lowering and -fails closed because the selected generic target profile cannot lower the Core -`let` nodes emitted for the digest-bound helper and conditional. No package is -emitted and Echo is not invoked. That failure is the routing evidence required -by #296: the next owner is Edict's generic Core-to-Target-IR boundary, not Echo -and not a native Jedit planner. +With Edict #201, the public application build lowers those Core `let` nodes into +generic, source-ordered Target IR and independently verifies the compiler-owned +result projection. The exact Echo provider package then refuses the invocation +as `InvalidProviderInvocation` because its published adapter, target +configuration, Target IR, and result-projection contracts do not yet admit the +new generic artifact family. No executable operation package is emitted and no +Echo evaluator runs. + +That refusal is the current routing evidence required by #296: Edict now +preserves the bounded pure program without learning Jedit vocabulary, and the +next owner is Echo's generic provider package and execution profile. It is not +evidence that `ReplaceRange` mutates a rope or that Jim runs end to end. ## Reproduce @@ -38,5 +44,7 @@ ECHO_REPO=/path/to/echo \ The script first republishes `jedit.text@1` through Edict's public lawpack authoring boundary, then copies Echo's checked provider package into the disposable application build tree and invokes Edict's public application build. -Until the generic lowering gap is implemented, it requires the stable -`TargetLoweringFailed` refusal and rejects accidental package emission. +Against Edict #201 it requires the stable `InvalidProviderInvocation` refusal +and rejects accidental package emission. Once Echo admits this generic artifact +family, the gate must advance again rather than treating provider refusal as a +permanent success condition. diff --git a/edict/replace-range/tests/build.sh b/edict/replace-range/tests/build.sh index 27f75ee..88fface 100755 --- a/edict/replace-range/tests/build.sh +++ b/edict/replace-range/tests/build.sh @@ -46,7 +46,7 @@ printf '%s\n' "$build_output" test "$build_status" -ne 0 printf '%s\n' "$build_output" \ | jq --exit-status --slurp \ - 'any(.[]; .type == "diagnostic" and .kind == "TargetLoweringFailed")' \ + 'any(.[]; .type == "diagnostic" and .kind == "InvalidProviderInvocation")' \ >/dev/null test ! -e .build/application/executable-operation-package.cbor test ! -e .build/application/verification-report.cbor From 629337d21c9708f515500b0a332c23bb13f8761e Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 05:05:03 -0700 Subject: [PATCH 3/9] feat: select Echo generic pure program profile --- CHANGELOG.md | 6 +++--- edict/replace-range/README.md | 16 +++++++++------- edict/replace-range/edict.lawpack.json | 7 +++---- edict/replace-range/src/ReplaceRange.edict | 2 +- edict/replace-range/tests/build.sh | 5 ++++- .../vendor/jedit-text/adapter.cbor | Bin 431 -> 446 bytes .../vendor/jedit-text/adapter.sha256 | 2 +- .../echo-operation-configuration.cbor | 2 +- .../echo-operation-configuration.sha256 | 2 +- .../vendor/jedit-text/edict.lawpack-output.json | 16 ++++++++-------- .../vendor/jedit-text/manifest.cbor | Bin 787 -> 787 bytes .../vendor/jedit-text/manifest.sha256 | 2 +- 12 files changed, 32 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7578c..2a6c86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ - Added the Jedit-owned `ReplaceRange.edict` application root and canonical `jedit.text@1` lawpack publication. With Edict #201, its executable integration gate now proves that real source lowers into generic pure Target IR and reaches - Echo's provider-contract boundary, which refuses the new artifact family - before package emission. Rope mutation and end-to-end Jim execution remain - unimplemented. + Echo's checked lowerer through the exact generic pure target configuration. + The lowerer returns typed `UnsupportedSemantics` before package emission. + Rope mutation and end-to-end Jim execution remain unimplemented. - Made `ReplaceRange` corroborate the selected Head/root byte extent and exact requested-range materialization before no-op classification, so truncated or absent retained roots fail with a typed malformed-rope obstruction. diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index e7300ad..82cb80a 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -21,9 +21,10 @@ traverse or rewrite a rope yet. With Edict #201, the public application build lowers those Core `let` nodes into generic, source-ordered Target IR and independently verifies the compiler-owned result projection. The exact Echo provider package then refuses the invocation -as `InvalidProviderInvocation` because its published adapter, target -configuration, Target IR, and result-projection contracts do not yet admit the -new generic artifact family. No executable operation package is emitted and no +as `ProviderLowererRefused / UnsupportedSemantics`. Echo now admits the exact +zero-choice generic pure target configuration, so the request reaches the +checked lowerer; that lowerer does not yet implement the corresponding generic +executable package profile. No executable operation package is emitted and no Echo evaluator runs. That refusal is the current routing evidence required by #296: Edict now @@ -44,7 +45,8 @@ ECHO_REPO=/path/to/echo \ The script first republishes `jedit.text@1` through Edict's public lawpack authoring boundary, then copies Echo's checked provider package into the disposable application build tree and invokes Edict's public application build. -Against Edict #201 it requires the stable `InvalidProviderInvocation` refusal -and rejects accidental package emission. Once Echo admits this generic artifact -family, the gate must advance again rather than treating provider refusal as a -permanent success condition. +Against Edict #201 and Echo #724 it requires the stable +`ProviderLowererRefused / UnsupportedSemantics` refusal and rejects accidental +package emission. Once Echo implements the generic pure package profile, the +gate must advance again rather than treating provider refusal as a permanent +success condition. diff --git a/edict/replace-range/edict.lawpack.json b/edict/replace-range/edict.lawpack.json index 81865aa..363da34 100644 --- a/edict/replace-range/edict.lawpack.json +++ b/edict/replace-range/edict.lawpack.json @@ -182,12 +182,11 @@ "localResources": [ { "name": "echo-config", - "coordinate": "jedit.text.echo-config/v1", + "coordinate": "echo.operation-lowering-configuration/v1", "output": "echo-operation-configuration.cbor", "value": { - "apiVersion": "jedit.text.echo-config/v1", - "instructionProfile": "echo.dpo@1.generic-bounded-graph", - "operation": "jedit.text@1.replaceRange" + "apiVersion": "echo.operation-lowering-configuration/v1", + "programKind": "compiler-produced-bounded-pure/v1" } }, { diff --git a/edict/replace-range/src/ReplaceRange.edict b/edict/replace-range/src/ReplaceRange.edict index cd317ee..6dc12a4 100644 --- a/edict/replace-range/src/ReplaceRange.edict +++ b/edict/replace-range/src/ReplaceRange.edict @@ -1,6 +1,6 @@ package jedit.text.replace_range@1; -use lawpack jedit.text@1 digest "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" as text; +use lawpack jedit.text@1 digest "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" as text; type ReplaceRangeInput = { bufferId: Bytes, diff --git a/edict/replace-range/tests/build.sh b/edict/replace-range/tests/build.sh index 88fface..72436a5 100755 --- a/edict/replace-range/tests/build.sh +++ b/edict/replace-range/tests/build.sh @@ -46,7 +46,10 @@ printf '%s\n' "$build_output" test "$build_status" -ne 0 printf '%s\n' "$build_output" \ | jq --exit-status --slurp \ - 'any(.[]; .type == "diagnostic" and .kind == "InvalidProviderInvocation")' \ + 'any(.[]; + .type == "diagnostic" + and .kind == "ProviderLowererRefused" + and (.message | contains("UnsupportedSemantics")))' \ >/dev/null test ! -e .build/application/executable-operation-package.cbor test ! -e .build/application/verification-report.cbor diff --git a/edict/replace-range/vendor/jedit-text/adapter.cbor b/edict/replace-range/vendor/jedit-text/adapter.cbor index c96488df0963dbf0b2283b73bd06eb953530e455..99e3ce6283224b6b1bcd4fa3d52055c2d893d555 100644 GIT binary patch delta 121 zcmZ3_ypMT3#XMX@glC#?SDLQ+(EX7c45Io@?;W@cq$bV+S+Y-Md_ MZggREX>V?Gpb>5@_5c6? diff --git a/edict/replace-range/vendor/jedit-text/adapter.sha256 b/edict/replace-range/vendor/jedit-text/adapter.sha256 index 152492e..4260e28 100644 --- a/edict/replace-range/vendor/jedit-text/adapter.sha256 +++ b/edict/replace-range/vendor/jedit-text/adapter.sha256 @@ -1 +1 @@ -sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828 +sha256:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049 diff --git a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor index b2057f9..ee60987 100644 --- a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor +++ b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.cbor @@ -1 +1 @@ -£ioperationxjedit.text@1.replaceRangejapiVersionxjedit.text.echo-config/v1rinstructionProfilex echo.dpo@1.generic-bounded-graph \ No newline at end of file +¢japiVersionx(echo.operation-lowering-configuration/v1kprogramKindx!compiler-produced-bounded-pure/v1 \ No newline at end of file diff --git a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 index 2612bf6..a708943 100644 --- a/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 +++ b/edict/replace-range/vendor/jedit-text/echo-operation-configuration.sha256 @@ -1 +1 @@ -sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de +sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4 diff --git a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json index 4f76b71..f95011b 100644 --- a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json +++ b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json @@ -7,13 +7,13 @@ "path": "manifest.cbor", "kind": "manifest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" + "digest": "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" }, { "path": "manifest.sha256", "kind": "manifestDigest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:0719a4161eaede5aa062f6cbd2e2ff74dee0bd961d0945449cd27ae2ef26f59a" + "digest": "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" }, { "path": "exports.cbor", @@ -30,14 +30,14 @@ { "path": "echo-operation-configuration.cbor", "kind": "localResource", - "coordinate": "jedit.text.echo-config/v1", - "digest": "sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de" + "coordinate": "echo.operation-lowering-configuration/v1", + "digest": "sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4" }, { "path": "echo-operation-configuration.sha256", "kind": "localResourceDigest", - "coordinate": "jedit.text.echo-config/v1", - "digest": "sha256:1ae7e97df9b46983591bc6cff08d537cd6a29c07f2cb2407d04f66f2e51b39de" + "coordinate": "echo.operation-lowering-configuration/v1", + "digest": "sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4" }, { "path": "verifier-rules.cbor", @@ -55,13 +55,13 @@ "path": "adapter.cbor", "kind": "adapter", "coordinate": "jedit.text.echo-adapter/v1", - "digest": "sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828" + "digest": "sha256:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049" }, { "path": "adapter.sha256", "kind": "adapterDigest", "coordinate": "jedit.text.echo-adapter/v1", - "digest": "sha256:4a4ead6b31d561c12d9abcd64b9e6cf518e4ee3d3d3770ae971559642fb02828" + "digest": "sha256:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049" } ] } diff --git a/edict/replace-range/vendor/jedit-text/manifest.cbor b/edict/replace-range/vendor/jedit-text/manifest.cbor index dac00a5ab49fa14f63804e195860ade1c1416bea..9acdcd3882ef72d06c4664f7a2d6886f595fba04 100644 GIT binary patch delta 45 zcmV+|0Mh@H29pM`w*ery+3{ZSjmeggs~!VPqza3Q#VQ-YvUQgzPafN^gz!nT*8w~M D?i&>m delta 45 zcmV+|0Mh@H29pM`w*eqZPOWP()nUOcn!MIao^16P Date: Sun, 23 Aug 2026 05:51:09 -0700 Subject: [PATCH 4/9] test: require verified ReplaceRange package --- CHANGELOG.md | 5 +- edict/replace-range/README.md | 32 +++++------ .../tests/assert-build-output.mjs | 54 +++++++++++++++++++ edict/replace-range/tests/build.sh | 25 ++------- 4 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 edict/replace-range/tests/assert-build-output.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6c86a..0df5095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ `jedit.text@1` lawpack publication. With Edict #201, its executable integration gate now proves that real source lowers into generic pure Target IR and reaches Echo's checked lowerer through the exact generic pure target configuration. - The lowerer returns typed `UnsupportedSemantics` before package emission. - Rope mutation and end-to-end Jim execution remain unimplemented. + Echo's generic pure-program lowerer now emits the exact compiler-produced + package, and its structurally separate verifier independently accepts the + package relation. Runtime evaluation and rope mutation remain unimplemented. - Made `ReplaceRange` corroborate the selected Head/root byte extent and exact requested-range materialization before no-op classification, so truncated or absent retained roots fail with a typed malformed-rope obstruction. diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index 82cb80a..0e096c3 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -20,17 +20,17 @@ traverse or rewrite a rope yet. With Edict #201, the public application build lowers those Core `let` nodes into generic, source-ordered Target IR and independently verifies the compiler-owned -result projection. The exact Echo provider package then refuses the invocation -as `ProviderLowererRefused / UnsupportedSemantics`. Echo now admits the exact -zero-choice generic pure target configuration, so the request reaches the -checked lowerer; that lowerer does not yet implement the corresponding generic -executable package profile. No executable operation package is emitted and no -Echo evaluator runs. - -That refusal is the current routing evidence required by #296: Edict now -preserves the bounded pure program without learning Jedit vocabulary, and the -next owner is Echo's generic provider package and execution profile. It is not -evidence that `ReplaceRange` mutates a rope or that Jim runs end to end. +result projection. Echo #724 now emits a distinct +`compiler-produced-bounded-pure/v1` executable package containing the exact +Core, lawpack exports, Target IR, and result projection. Echo's structurally +separate verifier independently reconstructs that package relation and emits an +accepted report. + +That accepted package is the current routing evidence required by #296: Edict +preserves the bounded pure program without learning Jedit vocabulary, and Echo +packages it without learning `ReplaceRange`. No Echo evaluator runs, no graph +or rope is mutated, and no Tick is settled. This is not evidence that +`ReplaceRange` mutates a rope or that Jim runs end to end. ## Reproduce @@ -45,8 +45,8 @@ ECHO_REPO=/path/to/echo \ The script first republishes `jedit.text@1` through Edict's public lawpack authoring boundary, then copies Echo's checked provider package into the disposable application build tree and invokes Edict's public application build. -Against Edict #201 and Echo #724 it requires the stable -`ProviderLowererRefused / UnsupportedSemantics` refusal and rejects accidental -package emission. Once Echo implements the generic pure package profile, the -gate must advance again rather than treating provider refusal as a permanent -success condition. +Against Edict #201 and Echo #724 it requires the generic pure package and an +exact accepted independent-verifier report. It also inspects the embedded +compiler artifacts and application coordinate. The gate must advance again +when Echo implements generic pure evaluation; package acceptance is not a +permanent substitute for runtime evidence. diff --git a/edict/replace-range/tests/assert-build-output.mjs b/edict/replace-range/tests/assert-build-output.mjs new file mode 100644 index 0000000..fb5f68d --- /dev/null +++ b/edict/replace-range/tests/assert-build-output.mjs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +// © James Ross Ω FLYING•ROBOTS + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +import { decode } from "cbor-x"; + +const [outputDirectory] = process.argv.slice(2); +assert.ok(outputDirectory, "pass the Edict application output directory"); + +const packageBytes = await readFile( + path.join(outputDirectory, "executable-operation-package.cbor"), +); +const reportBytes = await readFile( + path.join(outputDirectory, "verification-report.cbor"), +); +const executablePackage = decode(packageBytes); +const report = decode(reportBytes); +const program = decode(executablePackage.program); + +assert.equal(executablePackage.schema, "echo.operation-package/v1"); +assert.equal( + executablePackage.package_kind, + "compiler-produced-bounded-pure/v1", +); +assert.equal( + executablePackage.operation_coordinate, + "jedit.text.replace_range@1.replaceRange", +); +assert.equal(program.schema, "echo.compiler-produced-pure-program/v1"); +assert.equal(program.kind, "compiler-produced-bounded-pure/v1"); +assert.equal(program.intent, "replaceRange"); +for (const artifact of [ + "core_artifact", + "lawpack_exports_artifact", + "result_projection_artifact", + "target_ir_artifact", +]) { + assert.ok(Buffer.isBuffer(program[artifact]), `${artifact} must be retained`); + assert.ok(program[artifact].length > 0, `${artifact} must not be empty`); +} + +assert.equal( + report.apiVersion, + "echo.operation-package-verifier-report/v1", +); +assert.equal(report.outcome, "accepted"); +assert.equal(report.diagnosticBytes.length, 0); +assert.equal( + report.applicationResultProjection.id, + "jedit.text.replace_range@1.replaceRange", +); diff --git a/edict/replace-range/tests/build.sh b/edict/replace-range/tests/build.sh index 72436a5..74a5c59 100755 --- a/edict/replace-range/tests/build.sh +++ b/edict/replace-range/tests/build.sh @@ -33,23 +33,8 @@ if find .build/echo-provider -type l -print -quit | grep -q .; then exit 1 fi -set +e -build_output=$( - printf '%s\n' \ - '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","application":"edict.application.json"}' \ - | "$edict_bin" 2>&1 -) -build_status=$? -set -e - -printf '%s\n' "$build_output" -test "$build_status" -ne 0 -printf '%s\n' "$build_output" \ - | jq --exit-status --slurp \ - 'any(.[]; - .type == "diagnostic" - and .kind == "ProviderLowererRefused" - and (.message | contains("UnsupportedSemantics")))' \ - >/dev/null -test ! -e .build/application/executable-operation-package.cbor -test ! -e .build/application/verification-report.cbor +printf '%s\n' \ + '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","application":"edict.application.json"}' \ + | "$edict_bin" + +node tests/assert-build-output.mjs .build/application From 2283d3b4528c8bb27df5d1ed287552d0b12dba3f Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 15:34:07 -0700 Subject: [PATCH 5/9] test: lock ReplaceRange toolchain inputs --- CHANGELOG.md | 5 +- edict/replace-range/README.md | 23 +- edict/replace-range/edict.toolchain-lock.json | 28 ++ edict/replace-range/tests/build.sh | 35 +-- edict/replace-range/tests/package-chain.mjs | 252 ++++++++++++++++++ .../tests/proof-harness.spec.mjs | 195 ++++++++++++++ 6 files changed, 495 insertions(+), 43 deletions(-) create mode 100644 edict/replace-range/edict.toolchain-lock.json create mode 100644 edict/replace-range/tests/package-chain.mjs create mode 100644 edict/replace-range/tests/proof-harness.spec.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df5095..c36e201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ Echo's checked lowerer through the exact generic pure target configuration. Echo's generic pure-program lowerer now emits the exact compiler-produced package, and its structurally separate verifier independently accepts the - package relation. Runtime evaluation and rope mutation remain unimplemented. + package relation. The integration gate pins clean Edict and Echo revisions, + verifies the committed lawpack closure without repairing it, and rejects + authoritative-input mutation outside its disposable build root. Runtime + evaluation and rope mutation remain unimplemented. - Made `ReplaceRange` corroborate the selected Head/root byte extent and exact requested-range materialization before no-op classification, so truncated or absent retained roots fail with a typed malformed-rope obstruction. diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index 0e096c3..98aebc4 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -42,11 +42,18 @@ ECHO_REPO=/path/to/echo \ ./edict/replace-range/tests/build.sh ``` -The script first republishes `jedit.text@1` through Edict's public lawpack -authoring boundary, then copies Echo's checked provider package into the -disposable application build tree and invokes Edict's public application build. -Against Edict #201 and Echo #724 it requires the generic pure package and an -exact accepted independent-verifier report. It also inspects the embedded -compiler artifacts and application coordinate. The gate must advance again -when Echo implements generic pure evaluation; package acceptance is not a -permanent substitute for runtime evidence. +`edict.toolchain-lock.json` pins the exact Edict commit and CLI release, Rust +toolchain, Echo commit, provider identity, provider manifest bytes, and lowerer +and verifier components. The script refuses non-Git roots, wrong commits, or +dirty checkouts before invoking either toolchain. + +The script verifies the committed `jedit.text@1` closure through Edict's public +lawpack `checkOnly` boundary. It never republishes or repairs that authoritative +tree. It then copies Echo's checked provider package into `.build/`, invokes +Edict's public application build, and requires the generic pure package and an +accepted independent-verifier report. Snapshots cover the application inputs +outside `.build/` plus every tracked Edict and Echo file; any content, identity, +or timestamp mutation fails the run. Only `.build/` is disposable output. + +The gate must advance again when Echo implements generic pure evaluation; +package acceptance is not a permanent substitute for runtime evidence. diff --git a/edict/replace-range/edict.toolchain-lock.json b/edict/replace-range/edict.toolchain-lock.json new file mode 100644 index 0000000..7900d33 --- /dev/null +++ b/edict/replace-range/edict.toolchain-lock.json @@ -0,0 +1,28 @@ +{ + "schema": "jedit.edict-toolchain-lock/v1", + "rust": { + "toolchain": "1.94.0", + "rustcVersion": "rustc 1.94.0 (4a4ef493e 2026-03-02)", + "cargoVersion": "cargo 1.94.0 (85eff7c80 2026-01-15)" + }, + "edict": { + "commit": "adc1bf6da7d90fd93f135eea47380a4c68758479", + "cliVersion": "0.11.0-alpha.1" + }, + "echo": { + "commit": "37dd65a0f9e6e40a4aa1303f82d0f0892eefb06c", + "provider": { + "coordinate": "echo.edict-provider@1", + "digest": "sha256:b888f0c64a53c68d18c2bd3212589cf2ed9da3fbd551a64a9006184db3dde71f", + "manifestSha256": "8bce11896166a7ef52ddd9d07a60cfdb426d57697ba16e266aee0cb0f188a4b2", + "lowerer": { + "coordinate": "echo.dpo.lowerer/component@1", + "sha256": "20fe92ce8d7bec67f772715cb1f0f3d9ac4f65fe382eff3952f3c368849419dd" + }, + "verifier": { + "coordinate": "echo.dpo.verifier/component@1", + "sha256": "58f9451831338fa39a73d53790fe839037f1a8a1fb5345ffefc334af3ec90413" + } + } + } +} diff --git a/edict/replace-range/tests/build.sh b/edict/replace-range/tests/build.sh index 74a5c59..00009a3 100755 --- a/edict/replace-range/tests/build.sh +++ b/edict/replace-range/tests/build.sh @@ -3,38 +3,5 @@ set -euo pipefail project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) application_root="$project_root/edict/replace-range" -edict_repo=${EDICT_REPO:?set EDICT_REPO to an exact Edict checkout} -echo_repo=${ECHO_REPO:?set ECHO_REPO to an exact Echo checkout} -edict_bin=${EDICT_BIN:-"$edict_repo/target/debug/edict"} -provider_source="$echo_repo/schemas/edict-provider/package/v1" -test -x "$edict_bin" -test -f "$provider_source/provider-manifest.echo.json" -test -f "$provider_source/components/lowerer.echo-dpo.component.wasm" -test -f "$provider_source/components/verifier.echo-dpo.component.wasm" - -if find "$provider_source" -type l -print -quit | grep -q .; then - echo "provider package must not contain symlinks" >&2 - exit 1 -fi - -cd "$application_root" - -printf '%s\n' \ - '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","lawpack":"edict.lawpack.json"}' \ - | "$edict_bin" - -rm -rf .build/echo-provider .build/application -mkdir -p .build/echo-provider -cp -RL "$provider_source/." .build/echo-provider/ - -if find .build/echo-provider -type l -print -quit | grep -q .; then - echo "copied provider package must not contain symlinks" >&2 - exit 1 -fi - -printf '%s\n' \ - '{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","application":"edict.application.json"}' \ - | "$edict_bin" - -node tests/assert-build-output.mjs .build/application +exec node "$application_root/tests/package-chain.mjs" diff --git a/edict/replace-range/tests/package-chain.mjs b/edict/replace-range/tests/package-chain.mjs new file mode 100644 index 0000000..3133466 --- /dev/null +++ b/edict/replace-range/tests/package-chain.mjs @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +// © James Ross Ω FLYING•ROBOTS + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + cp, + lstat, + mkdir, + readFile, + readlink, + realpath, + readdir, + rm, +} from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const testsDirectory = path.dirname(fileURLToPath(import.meta.url)); +const applicationRoot = path.resolve(testsDirectory, ".."); +const projectRoot = path.resolve(applicationRoot, "../.."); +const lockPath = path.join(applicationRoot, "edict.toolchain-lock.json"); +const edictRepository = process.env.EDICT_REPO; +const echoRepository = process.env.ECHO_REPO; + +function run(command, arguments_, options = {}) { + const result = spawnSync(command, arguments_, { + cwd: options.cwd, + encoding: "utf8", + env: process.env, + input: options.input, + timeout: options.timeout ?? 120_000, + }); + if (options.emit !== false && result.stdout) { + process.stdout.write(result.stdout); + } + if (options.emit !== false && result.stderr) { + process.stderr.write(result.stderr); + } + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`${command} exited with status ${result.status}`); + } + return result.stdout.trim(); +} + +function git(repository, ...arguments_) { + return run("git", ["-C", repository, ...arguments_], { emit: false }); +} + +async function sha256(filePath) { + return createHash("sha256").update(await readFile(filePath)).digest("hex"); +} + +async function verifyGitCheckout(repository, expectedCommit, label) { + assert.ok(repository, `${label} repository path is required`); + const [requestedRoot, discoveredRoot] = await Promise.all([ + realpath(repository), + realpath(git(repository, "rev-parse", "--show-toplevel")), + ]); + assert.equal(discoveredRoot, requestedRoot, `${label} path must be the checkout root`); + assert.equal(git(repository, "rev-parse", "HEAD"), expectedCommit, `${label} commit mismatch`); + assert.equal( + git(repository, "status", "--porcelain=v1", "--untracked-files=all"), + "", + `${label} checkout must be clean`, + ); +} + +function providerArtifact(manifest, role) { + const matches = manifest.artifacts.filter((artifact) => artifact.role === role); + assert.equal(matches.length, 1, `provider must contain one ${role} artifact`); + return matches[0]; +} + +async function verifyProvider(lock) { + const providerRoot = path.join( + echoRepository, + "schemas", + "edict-provider", + "package", + "v1", + ); + await rejectSymlinks(providerRoot); + const manifestPath = path.join(providerRoot, "provider-manifest.echo.json"); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + assert.equal(manifest.provider.coordinate, lock.coordinate); + assert.equal(manifest.provider.digest, lock.digest); + assert.equal(await sha256(manifestPath), lock.manifestSha256); + + for (const [role, fileName, expected] of [ + ["lowerer.echo-dpo", "lowerer.echo-dpo.component.wasm", lock.lowerer], + ["verifier.echo-dpo", "verifier.echo-dpo.component.wasm", lock.verifier], + ]) { + const artifact = providerArtifact(manifest, role); + assert.equal(artifact.resource.coordinate, expected.coordinate); + assert.equal(artifact.resource.digest, `sha256:${expected.sha256}`); + assert.equal(await sha256(path.join(providerRoot, "components", fileName)), expected.sha256); + } + return providerRoot; +} + +async function rejectSymlinks(root) { + for (const entry of await readdir(root, { withFileTypes: true })) { + const entryPath = path.join(root, entry.name); + assert.equal(entry.isSymbolicLink(), false, `symbolic link is forbidden: ${entryPath}`); + if (entry.isDirectory()) { + await rejectSymlinks(entryPath); + } + } +} + +async function collectDirectoryFiles(root, relative = "") { + const files = []; + const directory = path.join(root, relative); + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (relative === "" && entry.name === ".build") { + continue; + } + const entryRelative = path.join(relative, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectDirectoryFiles(root, entryRelative))); + } else { + files.push(entryRelative); + } + } + return files; +} + +function trackedFiles(repository) { + return git(repository, "ls-files", "-z") + .split("\0") + .filter(Boolean) + .sort(); +} + +async function snapshotFile(namespace, root, relative) { + const filePath = path.join(root, relative); + const metadata = await lstat(filePath, { bigint: true }); + const common = { + path: `${namespace}/${relative.split(path.sep).join("/")}`, + mode: metadata.mode.toString(), + size: metadata.size.toString(), + modifiedNanoseconds: metadata.mtimeNs.toString(), + inode: metadata.ino.toString(), + }; + if (metadata.isSymbolicLink()) { + return { ...common, kind: "symlink", target: await readlink(filePath) }; + } + if (metadata.isFile()) { + return { ...common, kind: "file", sha256: await sha256(filePath) }; + } + return { ...common, kind: "other" }; +} + +async function snapshotAuthoritativeInputs() { + const roots = [ + ["jedit", applicationRoot, (await collectDirectoryFiles(applicationRoot)).sort()], + ["edict", edictRepository, trackedFiles(edictRepository)], + ["echo", echoRepository, trackedFiles(echoRepository)], + ]; + const snapshot = []; + for (const [namespace, root, files] of roots) { + for (const relative of files) { + snapshot.push(await snapshotFile(namespace, root, relative)); + } + } + return snapshot; +} + +async function verifyVersions(lock) { + assert.equal(run("rustc", [`+${lock.rust.toolchain}`, "--version"]), lock.rust.rustcVersion); + assert.equal(run("cargo", [`+${lock.rust.toolchain}`, "--version"]), lock.rust.cargoVersion); + run( + "cargo", + [`+${lock.rust.toolchain}`, "build", "--locked", "-p", "edict-cli"], + { cwd: edictRepository }, + ); + const edictBinary = path.join(edictRepository, "target", "debug", "edict"); + const version = JSON.parse(run(edictBinary, ["--version"])); + assert.equal(version.version, lock.edict.cliVersion); + return edictBinary; +} + +async function build() { + const lock = JSON.parse(await readFile(lockPath, "utf8")); + assert.equal(lock.schema, "jedit.edict-toolchain-lock/v1"); + await verifyGitCheckout(edictRepository, lock.edict.commit, "Edict"); + await verifyGitCheckout(echoRepository, lock.echo.commit, "Echo"); + const providerSource = await verifyProvider(lock.echo.provider); + const before = await snapshotAuthoritativeInputs(); + let failure; + try { + const edictBinary = await verifyVersions(lock); + run(edictBinary, [], { + cwd: applicationRoot, + input: `${JSON.stringify({ + schema: "edict.compiler.settings/v1", + type: "compilerSettings", + operation: "build", + lawpack: "edict.lawpack.json", + checkOnly: true, + })}\n`, + }); + + const buildRoot = path.join(applicationRoot, ".build"); + const providerDestination = path.join(buildRoot, "echo-provider"); + const applicationOutput = path.join(buildRoot, "application"); + await rm(providerDestination, { recursive: true, force: true }); + await rm(applicationOutput, { recursive: true, force: true }); + await mkdir(providerDestination, { recursive: true }); + await cp(providerSource, providerDestination, { recursive: true }); + await rejectSymlinks(providerDestination); + + run(edictBinary, [], { + cwd: applicationRoot, + input: `${JSON.stringify({ + schema: "edict.compiler.settings/v1", + type: "compilerSettings", + operation: "build", + application: "edict.application.json", + })}\n`, + }); + run(process.execPath, [path.join(testsDirectory, "assert-build-output.mjs"), applicationOutput], { + cwd: applicationRoot, + }); + } catch (error) { + failure = error; + } + + const after = await snapshotAuthoritativeInputs(); + let mutationFailure; + try { + assert.deepEqual(after, before, "authoritative inputs changed during package-chain build"); + } catch (error) { + mutationFailure = error; + } + if (failure && mutationFailure) { + throw new AggregateError([failure, mutationFailure], "build failed and authoritative inputs changed"); + } + if (mutationFailure) { + throw mutationFailure; + } + if (failure) { + throw failure; + } +} + +await build(); diff --git a/edict/replace-range/tests/proof-harness.spec.mjs b/edict/replace-range/tests/proof-harness.spec.mjs new file mode 100644 index 0000000..b3fa1e4 --- /dev/null +++ b/edict/replace-range/tests/proof-harness.spec.mjs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +// © James Ross Ω FLYING•ROBOTS + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { cp, mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { decode, encode } from "cbor-x"; + +const testDirectory = path.dirname(fileURLToPath(import.meta.url)); +const applicationRoot = path.resolve(testDirectory, ".."); +const projectRoot = path.resolve(applicationRoot, "../.."); +const edictRepository = process.env.EDICT_REPO; +const echoRepository = process.env.ECHO_REPO; + +function requireToolchainEnvironment() { + assert.ok(edictRepository, "EDICT_REPO must name the exact Edict checkout"); + assert.ok(echoRepository, "ECHO_REPO must name the exact Echo checkout"); +} + +async function fixture() { + const root = await mkdtemp(path.join(os.tmpdir(), "jedit-replace-range-proof-")); + const fixtureProject = path.join(root, "jedit"); + const fixtureApplication = path.join(fixtureProject, "edict", "replace-range"); + await mkdir(path.dirname(fixtureApplication), { recursive: true }); + await cp(applicationRoot, fixtureApplication, { recursive: true }); + await symlink(path.join(projectRoot, "node_modules"), path.join(fixtureProject, "node_modules"), "dir"); + return { + root, + project: fixtureProject, + application: fixtureApplication, + async dispose() { + await rm(root, { recursive: true, force: true }); + }, + }; +} + +function runBuild(fixtureProject, overrides = {}) { + return spawnSync("bash", ["edict/replace-range/tests/build.sh"], { + cwd: fixtureProject, + encoding: "utf8", + env: { + ...process.env, + EDICT_REPO: edictRepository, + ECHO_REPO: echoRepository, + ...overrides, + }, + timeout: 120_000, + }); +} + +function assertCommandCompleted(result) { + assert.equal(result.error, undefined, result.error?.message); + assert.notEqual(result.status, null, `command terminated by ${result.signal}`); +} + +test("rejects_non_git_or_wrong_commit_toolchain_inputs", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const fakeEdict = path.join(subject.root, "fake-edict"); + const fakeEcho = path.join(subject.root, "fake-echo"); + const fakeEdictBinary = path.join(fakeEdict, "target", "debug", "edict"); + const providerSource = path.join( + echoRepository, + "schemas", + "edict-provider", + "package", + "v1", + ); + const fakeProvider = path.join( + fakeEcho, + "schemas", + "edict-provider", + "package", + "v1", + ); + await mkdir(path.dirname(fakeEdictBinary), { recursive: true }); + await mkdir(path.dirname(fakeProvider), { recursive: true }); + await symlink(path.join(edictRepository, "target", "debug", "edict"), fakeEdictBinary); + await cp(providerSource, fakeProvider, { recursive: true }); + + const result = runBuild(subject.project, { + EDICT_REPO: fakeEdict, + ECHO_REPO: fakeEcho, + }); + assertCommandCompleted(result); + assert.notEqual( + result.status, + 0, + "non-Git toolchain directories must be rejected before publication or application build", + ); + } finally { + await subject.dispose(); + } +}); + +test("lawpack_check_only_rejects_drift_without_repairing_it", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const digestPath = path.join( + subject.application, + "vendor", + "jedit-text", + "manifest.sha256", + ); + const corruptDigest = `sha256:${"0".repeat(64)}\n`; + await writeFile(digestPath, corruptDigest); + + const result = runBuild(subject.project); + assertCommandCompleted(result); + assert.notEqual(result.status, 0, "drift must fail instead of being repaired"); + assert.equal( + await readFile(digestPath, "utf8"), + corruptDigest, + "check-only validation must leave the drifted artifact untouched", + ); + } finally { + await subject.dispose(); + } +}); + +test( + "rejects_package_whose_recomputed_identity_differs_from_report_subject", + { timeout: 120_000 }, + async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const build = runBuild(subject.project); + assertCommandCompleted(build); + assert.equal(build.status, 0, build.stderr); + + const outputDirectory = path.join(subject.application, ".build", "application"); + const packagePath = path.join(outputDirectory, "executable-operation-package.cbor"); + const executablePackage = decode(await readFile(packagePath)); + const program = decode(executablePackage.program); + const alteredCore = Buffer.from(program.core_artifact); + alteredCore[0] ^= 1; + program.core_artifact = alteredCore; + executablePackage.program = encode(program); + await writeFile(packagePath, encode(executablePackage)); + + const assertion = spawnSync( + process.execPath, + [path.join(subject.application, "tests", "assert-build-output.mjs"), outputDirectory], + { cwd: subject.project, encoding: "utf8", timeout: 30_000 }, + ); + assertCommandCompleted(assertion); + assert.notEqual( + assertion.status, + 0, + "a report for the original package must not authorize altered embedded Core bytes", + ); + } finally { + await subject.dispose(); + } + }, +); + +test("rejects_short_buffer_and_head_identities", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const build = runBuild(subject.project); + assertCommandCompleted(build); + assert.equal(build.status, 0, build.stderr); + + const packageBytes = await readFile( + path.join(subject.application, ".build", "application", "executable-operation-package.cbor"), + ); + const executablePackage = decode(packageBytes); + const program = decode(executablePackage.program); + const core = decode(program.core_artifact); + for (const typeName of [ + "ReplaceRangeInput.bufferId", + "ReplaceRangeInput.basisHeadId", + "ReplaceRangeBoundary.bufferId", + "ReplaceRangeBoundary.basisHeadId", + ]) { + assert.deepEqual( + core.types[typeName], + { kind: "Bytes", min: 32, max: 32 }, + `${typeName} must preserve the exact 32-byte identity contract in Core`, + ); + } + } finally { + await subject.dispose(); + } +}); From a8d4a527c42bc04b3260a402f34c03cf44fde569 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 16:16:45 -0700 Subject: [PATCH 6/9] Bind ReplaceRange to exact text identities --- edict/replace-range/edict.lawpack.json | 10 +++++++++- edict/replace-range/edict.toolchain-lock.json | 10 +++++----- edict/replace-range/src/ReplaceRange.edict | 14 +++++++------- .../jedit-text/edict.lawpack-output.json | 8 ++++---- .../vendor/jedit-text/exports.cbor | Bin 2212 -> 2342 bytes .../vendor/jedit-text/exports.sha256 | 2 +- .../vendor/jedit-text/manifest.cbor | Bin 787 -> 787 bytes .../vendor/jedit-text/manifest.sha256 | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/edict/replace-range/edict.lawpack.json b/edict/replace-range/edict.lawpack.json index 363da34..13483ef 100644 --- a/edict/replace-range/edict.lawpack.json +++ b/edict/replace-range/edict.lawpack.json @@ -14,7 +14,15 @@ "types": [ { "coordinate": "jedit.text@1.NodeId", - "definition": "Bytes" + "definition": "Bytes" + }, + { + "coordinate": "jedit.text@1.BufferId", + "definition": "jedit.text@1.NodeId" + }, + { + "coordinate": "jedit.text@1.HeadId", + "definition": "jedit.text@1.NodeId" }, { "coordinate": "jedit.text@1.ReplacementBytes", diff --git a/edict/replace-range/edict.toolchain-lock.json b/edict/replace-range/edict.toolchain-lock.json index 7900d33..b95983c 100644 --- a/edict/replace-range/edict.toolchain-lock.json +++ b/edict/replace-range/edict.toolchain-lock.json @@ -6,18 +6,18 @@ "cargoVersion": "cargo 1.94.0 (85eff7c80 2026-01-15)" }, "edict": { - "commit": "adc1bf6da7d90fd93f135eea47380a4c68758479", + "commit": "68633c409474b9dffac4a33a674b2a3f29ad865c", "cliVersion": "0.11.0-alpha.1" }, "echo": { - "commit": "37dd65a0f9e6e40a4aa1303f82d0f0892eefb06c", + "commit": "ce032c8e07b2e3ffe82d97903a565521257cede6", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:b888f0c64a53c68d18c2bd3212589cf2ed9da3fbd551a64a9006184db3dde71f", - "manifestSha256": "8bce11896166a7ef52ddd9d07a60cfdb426d57697ba16e266aee0cb0f188a4b2", + "digest": "sha256:319b2f44db2fdc953ab4dd4d7603a60db8c5951cc62a0ac2e6b41797eebcee92", + "manifestSha256": "b6687034723e965006b4ffbfe2b1848d4c173ad6528964d060954c75e14e7259", "lowerer": { "coordinate": "echo.dpo.lowerer/component@1", - "sha256": "20fe92ce8d7bec67f772715cb1f0f3d9ac4f65fe382eff3952f3c368849419dd" + "sha256": "7d00aa4d4ec63fb81dad318b0165010cd4451f33b3f939f90a0457caf54a46eb" }, "verifier": { "coordinate": "echo.dpo.verifier/component@1", diff --git a/edict/replace-range/src/ReplaceRange.edict b/edict/replace-range/src/ReplaceRange.edict index 6dc12a4..0c77483 100644 --- a/edict/replace-range/src/ReplaceRange.edict +++ b/edict/replace-range/src/ReplaceRange.edict @@ -1,21 +1,21 @@ package jedit.text.replace_range@1; -use lawpack jedit.text@1 digest "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" as text; +use lawpack jedit.text@1 digest "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" as text; type ReplaceRangeInput = { - bufferId: Bytes, - basisHeadId: Bytes, + bufferId: text.BufferId, + basisHeadId: text.HeadId, startByte: U64, endByte: U64, - replacement: Bytes, + replacement: text.ReplacementBytes, }; type ReplaceRangeBoundary = { - bufferId: Bytes, - basisHeadId: Bytes, + bufferId: text.BufferId, + basisHeadId: text.HeadId, startByte: U64, endByte: U64, - replacement: Bytes, + replacement: text.ReplacementBytes, rangeIsEmpty: U32, createdLeafCeiling: U64, }; diff --git a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json index f95011b..0c5d525 100644 --- a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json +++ b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json @@ -7,25 +7,25 @@ "path": "manifest.cbor", "kind": "manifest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" + "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" }, { "path": "manifest.sha256", "kind": "manifestDigest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197" + "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" }, { "path": "exports.cbor", "kind": "exports", "coordinate": "jedit.text.exports/v1", - "digest": "sha256:2a4ef08bec9f2839aa69841a6458eabc037f1ea698fd1a68392aeb84d5b306be" + "digest": "sha256:ffa6529b8327ab49e7cf62016e940e5cabf90c5468d9d1efb0dcb3d7d6574c88" }, { "path": "exports.sha256", "kind": "exportsDigest", "coordinate": "jedit.text.exports/v1", - "digest": "sha256:2a4ef08bec9f2839aa69841a6458eabc037f1ea698fd1a68392aeb84d5b306be" + "digest": "sha256:ffa6529b8327ab49e7cf62016e940e5cabf90c5468d9d1efb0dcb3d7d6574c88" }, { "path": "echo-operation-configuration.cbor", diff --git a/edict/replace-range/vendor/jedit-text/exports.cbor b/edict/replace-range/vendor/jedit-text/exports.cbor index ab18a6b613d15e096bb3f507ba661ab3fe82ed6a..65bcda4a8cee4eb7fe8f3ee4237e28d580797495 100644 GIT binary patch delta 166 zcmZ1?xJ-y+S!zjTL27a9L=Nrxe5cBi)MA^|ip1m+TVo@;MOn%D`9&$2d5I;drCF&d znI(E9sTCy-hI&q=X=$lNo+()=scD&cnI)O|dBrFS{PI&$JyVbkK$Z7MO-w=8E{J3S KME^#|WDWqyH9rFY delta 40 wcmZ1`v_z0&S!zjTL27aHL=J8KT&K#C)MA_5#0pzuBfCYDvzVke-goB!04!DxeE!v^ B6}$id delta 43 zcmV+`0M!4J29pMmLLe$m@Qdu9C^@QWgc@X6>bwJg9;TT68fZBx>x9*_2EMUku>zDq B6IcKM diff --git a/edict/replace-range/vendor/jedit-text/manifest.sha256 b/edict/replace-range/vendor/jedit-text/manifest.sha256 index ca2740e..b329237 100644 --- a/edict/replace-range/vendor/jedit-text/manifest.sha256 +++ b/edict/replace-range/vendor/jedit-text/manifest.sha256 @@ -1 +1 @@ -sha256:5c41ef4bec7f9f50b3ca3a5332969052f32745046200cfb2f89316d487e25197 +sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf From d8af4e2227c6e6ba1b2a27f9d09b4f4fe5781ae0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 16:36:45 -0700 Subject: [PATCH 7/9] Verify the exact executable subject --- edict/replace-range/edict.toolchain-lock.json | 10 +-- .../tests/assert-build-output.mjs | 84 ++++++++++++++++++- package-lock.json | 1 + package.json | 1 + 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/edict/replace-range/edict.toolchain-lock.json b/edict/replace-range/edict.toolchain-lock.json index b95983c..a0369e4 100644 --- a/edict/replace-range/edict.toolchain-lock.json +++ b/edict/replace-range/edict.toolchain-lock.json @@ -10,18 +10,18 @@ "cliVersion": "0.11.0-alpha.1" }, "echo": { - "commit": "ce032c8e07b2e3ffe82d97903a565521257cede6", + "commit": "27a56eb91964aeeea90d36c58585683b341a2dfc", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:319b2f44db2fdc953ab4dd4d7603a60db8c5951cc62a0ac2e6b41797eebcee92", - "manifestSha256": "b6687034723e965006b4ffbfe2b1848d4c173ad6528964d060954c75e14e7259", + "digest": "sha256:a4e0d9e31ada5c8c14adfce05fdecb228e5b79545f3574a5e21e7db71ab3f307", + "manifestSha256": "205bdba71fd552316be59079d7509c592b1ac98af7197a119fe56ce12ea0a5f6", "lowerer": { "coordinate": "echo.dpo.lowerer/component@1", - "sha256": "7d00aa4d4ec63fb81dad318b0165010cd4451f33b3f939f90a0457caf54a46eb" + "sha256": "a4758f060122fba8c073841ccaa22bf0b8a742b495b76eb33c914eff3250dcf5" }, "verifier": { "coordinate": "echo.dpo.verifier/component@1", - "sha256": "58f9451831338fa39a73d53790fe839037f1a8a1fb5345ffefc334af3ec90413" + "sha256": "174cf8758815bf2b9f2ef575517aa6d144ed3f2d1995fc65d25a211b9dea82d9" } } } diff --git a/edict/replace-range/tests/assert-build-output.mjs b/edict/replace-range/tests/assert-build-output.mjs index fb5f68d..4c66cd6 100644 --- a/edict/replace-range/tests/assert-build-output.mjs +++ b/edict/replace-range/tests/assert-build-output.mjs @@ -2,10 +2,26 @@ // © James Ross Ω FLYING•ROBOTS import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; -import { decode } from "cbor-x"; +import { decode, encode } from "cbor-x"; + +function canonicalArtifactDigest(domain, canonicalArtifactBytes) { + const preimage = Buffer.concat([ + Buffer.from([0x83]), + Buffer.from(encode("edict.digest/v1")), + Buffer.from(encode(domain)), + Buffer.from(canonicalArtifactBytes), + ]); + return createHash("sha256").update(preimage).digest(); +} + +function assertResourceReference(reference, coordinate, digest) { + assert.equal(reference.id, coordinate); + assert.deepEqual(reference.digest, ["sha256", digest]); +} const [outputDirectory] = process.argv.slice(2); assert.ok(outputDirectory, "pass the Edict application output directory"); @@ -20,6 +36,16 @@ const executablePackage = decode(packageBytes); const report = decode(reportBytes); const program = decode(executablePackage.program); +const packageDigest = canonicalArtifactDigest( + "echo.operation-package/v1", + packageBytes, +); +assertResourceReference( + report.package, + "executable-operation-package.echo", + packageDigest, +); + assert.equal(executablePackage.schema, "echo.operation-package/v1"); assert.equal( executablePackage.package_kind, @@ -42,6 +68,31 @@ for (const artifact of [ assert.ok(program[artifact].length > 0, `${artifact} must not be empty`); } +const coreDigest = canonicalArtifactDigest( + "edict.core.module/v1", + program.core_artifact, +); +assert.deepEqual(executablePackage.semantic_closure.core_identity, coreDigest); +assert.deepEqual( + executablePackage.semantic_closure.canonical_meaning_identity, + coreDigest, +); + +const targetIrDigest = canonicalArtifactDigest( + "edict.target-ir.artifact/v1", + program.target_ir_artifact, +); +assert.deepEqual( + executablePackage.semantic_closure.target_ir_identity, + targetIrDigest, +); +assertResourceReference(report.targetIr, "echo.span-ir/v1", targetIrDigest); + +const resultProjectionDigest = canonicalArtifactDigest( + "edict.result-projection.artifact/v1", + program.result_projection_artifact, +); + assert.equal( report.apiVersion, "echo.operation-package-verifier-report/v1", @@ -52,3 +103,34 @@ assert.equal( report.applicationResultProjection.id, "jedit.text.replace_range@1.replaceRange", ); +assertResourceReference( + report.applicationResultProjection, + "jedit.text.replace_range@1.replaceRange", + resultProjectionDigest, +); + +assert.equal( + report.executableSubject.reference.id, + "echo.executable-subject/v1", +); +assert.ok( + Buffer.isBuffer(report.executableSubject.bytes), + "the verifier report must retain the executable subject bytes", +); +const executableSubjectDigest = canonicalArtifactDigest( + "echo.executable-subject/v1", + report.executableSubject.bytes, +); +assertResourceReference( + report.executableSubject.reference, + "echo.executable-subject/v1", + executableSubjectDigest, +); +const executableSubject = decode(report.executableSubject.bytes); +assert.equal(executableSubject.apiVersion, "echo.executable-subject/v1"); +assert.deepEqual(executableSubject.package, report.package); +assert.deepEqual(executableSubject.targetIr, report.targetIr); +assert.deepEqual( + executableSubject.applicationResultProjection, + report.applicationResultProjection, +); diff --git a/package-lock.json b/package-lock.json index 801e5a1..d76f6e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@flyingrobots/bijou-i18n-tools-node": "^7.0.0", "@flyingrobots/bijou-i18n-tools-xlsx": "^7.0.0", "@types/node": "^22.0.0", + "cbor-x": "^1.6.4", "semver": "^7.8.5", "tsx": "^4.22.4", "typescript": "^5.9.3" diff --git a/package.json b/package.json index d0490ec..8161bb2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@flyingrobots/bijou-i18n-tools-node": "^7.0.0", "@flyingrobots/bijou-i18n-tools-xlsx": "^7.0.0", "@types/node": "^22.0.0", + "cbor-x": "^1.6.4", "semver": "^7.8.5", "tsx": "^4.22.4", "typescript": "^5.9.3" From d6769cc1acca7527114d9ca88a0719997956ff75 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 16:42:55 -0700 Subject: [PATCH 8/9] Require exact ReplaceRange package evidence --- .github/workflows/ci.yml | 76 +++++++- edict/replace-range/README.md | 26 ++- edict/replace-range/edict.build-lock.json | 106 +++++++++++ .../edict.executable-subject-lock.json | 20 +++ .../tests/assert-build-output.mjs | 165 +++++++++++++++++- .../tests/proof-harness.spec.mjs | 38 ++++ 6 files changed, 421 insertions(+), 10 deletions(-) create mode 100644 edict/replace-range/edict.build-lock.json create mode 100644 edict/replace-range/edict.executable-subject-lock.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8fc974..09b79e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,9 +168,79 @@ jobs: - name: Run Echo release gate run: npm run release-gate:jedit-echo + edict-replace-range: + name: edict / replace-range package chain + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout Jedit + uses: actions/checkout@v6.0.2 + with: + path: jedit + + - name: Read pinned toolchain commits + id: toolchain + working-directory: jedit + run: | + node --input-type=module <<'NODE' >> "$GITHUB_OUTPUT" + import { readFileSync } from "node:fs"; + const lock = JSON.parse( + readFileSync("edict/replace-range/edict.toolchain-lock.json", "utf8"), + ); + console.log(`edict_commit=${lock.edict.commit}`); + console.log(`echo_commit=${lock.echo.commit}`); + console.log(`rust_toolchain=${lock.rust.toolchain}`); + NODE + + - name: Checkout pinned Edict + uses: actions/checkout@v6.0.2 + with: + repository: flyingrobots/edict + ref: ${{ steps.toolchain.outputs.edict_commit }} + path: edict + + - name: Checkout pinned Echo + uses: actions/checkout@v6.0.2 + with: + repository: flyingrobots/echo + ref: ${{ steps.toolchain.outputs.echo_commit }} + path: echo + + - name: Setup Node.js + uses: actions/setup-node@v6.4.0 + with: + node-version: 22 + cache: 'npm' + cache-dependency-path: jedit/package-lock.json + + - name: Setup pinned Rust + run: rustup toolchain install '${{ steps.toolchain.outputs.rust_toolchain }}' --profile minimal + + - name: Install Jedit dependencies + working-directory: jedit + run: npm ci + + - name: Verify exact package chain and adversarial regressions + working-directory: jedit + env: + EDICT_REPO: ${{ github.workspace }}/edict + ECHO_REPO: ${{ github.workspace }}/echo + run: node --test edict/replace-range/tests/proof-harness.spec.mjs + + - name: Verify authoritative checkouts remain clean + run: | + for repository in jedit edict echo; do + if [[ -n "$(git -C "$repository" status --porcelain=v1 --untracked-files=all)" ]]; then + echo "$repository changed during the package-chain gate" >&2 + git -C "$repository" status --short >&2 + exit 1 + fi + done + check: name: check - needs: [ plan, build, test-shards, quality, release-gate ] + needs: [ plan, build, test-shards, quality, release-gate, edict-replace-range ] if: always() runs-on: ubuntu-latest @@ -182,9 +252,10 @@ jobs: TEST_SHARDS_RESULT: ${{ needs.test-shards.result }} QUALITY_RESULT: ${{ needs.quality.result }} RELEASE_GATE_RESULT: ${{ needs.release-gate.result }} + EDICT_REPLACE_RANGE_RESULT: ${{ needs.edict-replace-range.result }} run: | failed=0 - for result in "$PLAN_RESULT" "$BUILD_RESULT" "$QUALITY_RESULT"; do + for result in "$PLAN_RESULT" "$BUILD_RESULT" "$QUALITY_RESULT" "$EDICT_REPLACE_RANGE_RESULT"; do if [[ "$result" != "success" ]]; then failed=1 fi @@ -205,5 +276,6 @@ jobs: echo "| test shards | $TEST_SHARDS_RESULT |" echo "| quality | $QUALITY_RESULT |" echo "| release gate | $RELEASE_GATE_RESULT |" + echo "| edict / replace-range package chain | $EDICT_REPLACE_RANGE_RESULT |" } >> "$GITHUB_STEP_SUMMARY" exit "$failed" diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index 98aebc4..a063d07 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -1,11 +1,16 @@ # ReplaceRange Edict application This directory is the Jedit-owned application root for issue #296. It keeps -five artifact classes separate: +the authored, built, and evidentiary artifact classes separate: - `edict.lawpack.json` is the reviewable `jedit.text@1` authoring input; - `vendor/jedit-text/` is Edict's canonical published lawpack closure; - `src/ReplaceRange.edict` is the application-owned source; +- `edict.toolchain-lock.json` pins the exact build implementations and + provider; +- `edict.build-lock.json` pins the exact source closure and emitted artifact + chain; +- `edict.executable-subject-lock.json` pins the verified executable meaning; - `.build/` contains disposable provider and compiler outputs; - `contracts/jedit/lawpacks/replace-range-v1/` remains the independent schema and oracle corpus and is not executable input. @@ -13,10 +18,10 @@ five artifact classes separate: ## Current executable boundary The checked-in source is the first compiler-pressure slice, not a completed -text mutation. It binds bounded identity bytes, the selected basis, range, -replacement bytes, operation profile, budget, imported helper implementation, -and a pure conditional into Edict Core. It intentionally does not claim to -traverse or rewrite a rope yet. +text mutation. It binds exact-length nominal identities, the selected basis, +range, replacement bytes, operation profile, budget, imported helper +implementation, and a pure conditional into Edict Core. It intentionally does +not claim to traverse or rewrite a rope yet. With Edict #201, the public application build lowers those Core `let` nodes into generic, source-ordered Target IR and independently verifies the compiler-owned @@ -24,7 +29,7 @@ result projection. Echo #724 now emits a distinct `compiler-produced-bounded-pure/v1` executable package containing the exact Core, lawpack exports, Target IR, and result projection. Echo's structurally separate verifier independently reconstructs that package relation and emits an -accepted report. +accepted report bound to retained canonical `echo.executable-subject/v1` bytes. That accepted package is the current routing evidence required by #296: Edict preserves the bounded pure program without learning Jedit vocabulary, and Echo @@ -47,6 +52,15 @@ toolchain, Echo commit, provider identity, provider manifest bytes, and lowerer and verifier components. The script refuses non-Git roots, wrong commits, or dirty checkouts before invoking either toolchain. +`edict.build-lock.json` binds the exact source and validation closure to the +Core, Target IR, result projection, executable package, verification report, +and executable-subject identities. `edict.executable-subject-lock.json` +separately records the subject identity and the exact package, Target IR, and +result-projection references it contains. The harness recomputes every digest +from emitted bytes and refuses a valid report paired with a substituted package. +The locks contain no self-referential Jedit commit; an exact PR head remains an +external review coordinate. + The script verifies the committed `jedit.text@1` closure through Edict's public lawpack `checkOnly` boundary. It never republishes or repairs that authoritative tree. It then copies Echo's checked provider package into `.build/`, invokes diff --git a/edict/replace-range/edict.build-lock.json b/edict/replace-range/edict.build-lock.json new file mode 100644 index 0000000..75296cc --- /dev/null +++ b/edict/replace-range/edict.build-lock.json @@ -0,0 +1,106 @@ +{ + "schema": "jedit.edict-build-lock/v1", + "applicationCoordinate": "jedit.text.replace_range@1", + "operationCoordinate": "jedit.text.replace_range@1.replaceRange", + "toolchainLockSha256": "69c68a095278d10bc2215afc729dabcba937027da99938862cfb555b8bac9a93", + "sourceClosure": { + "schema": "jedit.edict-source-closure/v1", + "digest": "sha256:dfb61b22dba5616ed3259e49744534ea878aec6b053535c6793cd43e88e26a78", + "files": [ + { + "path": "edict.application.json", + "sha256": "3182577627cd1f88bf590f3a8481ef53476c83d783aa142182e7c65eb1f72f8b" + }, + { + "path": "edict.lawpack.json", + "sha256": "37e91e54ae54ed6852bcc736d0e49e93379321f55b77750b31b713bbb7f0e9d3" + }, + { + "path": "edict.toolchain-lock.json", + "sha256": "69c68a095278d10bc2215afc729dabcba937027da99938862cfb555b8bac9a93" + }, + { + "path": "src/ReplaceRange.edict", + "sha256": "7af4ee51415bb4391510c5e91a029838a425515abe172aad9c6805b2d70dd916" + }, + { + "path": "tests/assert-build-output.mjs", + "sha256": "a0e48f69ff7828fad421864bcf10ccfe830fa57264b2a95c25b979b0988d1235" + }, + { + "path": "tests/build.sh", + "sha256": "b5827a1e2a4b64b214406ee3d0cdc9499059922f9878649946c0358fbbd6ea6d" + }, + { + "path": "tests/package-chain.mjs", + "sha256": "3128f12beba257df3231681fcfc672e4fa4cb4436806e3d410ae577180e8afcf" + }, + { + "path": "tests/proof-harness.spec.mjs", + "sha256": "541b5599988ea66f85a807c7f249e7db488f7932ccf3765b94e7d12b1dc6c174" + }, + { + "path": "vendor/jedit-text/edict.lawpack-output.json", + "sha256": "2471666ac595570ea6f812aa613916e20d6f40fd0a99fafb35002be3f4fbcd74" + }, + { + "path": "vendor/jedit-text/exports.cbor", + "sha256": "dfee735fd0a7f714a401aa310973596e959672a11bcac5c7c1d289c9344a7a0d" + }, + { + "path": "vendor/jedit-text/exports.sha256", + "sha256": "3661e5416b155f803fa0ba888397e4f9b5c79b228ecda538b37b375d3a09791e" + }, + { + "path": "vendor/jedit-text/manifest.cbor", + "sha256": "dadfd41260b1e3449b9fc8c1e5f4c9523b6b819f36fcc1316b53b7aa943a7118" + }, + { + "path": "vendor/jedit-text/manifest.sha256", + "sha256": "0496d95c6a0c37e0395e20f7c5c063c0277b92353412b262e935d17d1141d31b" + } + ] + }, + "lawpackRelease": { + "coordinate": "jedit.text@1", + "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" + }, + "provider": { + "coordinate": "echo.edict-provider@1", + "digest": "sha256:a4e0d9e31ada5c8c14adfce05fdecb228e5b79545f3574a5e21e7db71ab3f307" + }, + "artifacts": { + "core": { + "coordinate": "jedit.text.replace_range@1", + "domain": "edict.core.module/v1", + "digest": "sha256:a2cec435e178b0f772508125a9c7c6a7a79adc05367cc37955e4a6aa7714c9ef" + }, + "targetIr": { + "coordinate": "echo.span-ir/v1", + "domain": "edict.target-ir.artifact/v1", + "digest": "sha256:84c1f9826285822f969fc287bc09af2afd63ca876f19f54e5c20ea2e7b424260" + }, + "resultProjection": { + "coordinate": "jedit.text.replace_range@1.replaceRange", + "domain": "edict.result-projection.artifact/v1", + "digest": "sha256:f804baa01c357e5ed54e86f752ea7f8201e454ad0b47c10813fde8c7dc2aeba6" + }, + "executablePackage": { + "coordinate": "executable-operation-package.echo", + "domain": "echo.operation-package/v1", + "digest": "sha256:6075cb3be2308d7a513a05637499ab130bb35296bbcf14064feb153c47612eb1", + "rawSha256": "5666555277c9f33a5c2bc3ec5f6859c8ee4005075bc1d6a457f12a384065a8f3" + }, + "verificationReport": { + "coordinate": "verifier-report.echo-operation", + "domain": "echo.operation-package-verifier-report/v1", + "digest": "sha256:410e10dfa931c57da3aaf64a50ef4b62a63217e97e6152cdc2a22a661685bfd9", + "rawSha256": "4038b28354fe8555a581fe6e87fec7edcd550e27bbe54693f997558b86da1a97" + }, + "executableSubject": { + "coordinate": "echo.executable-subject/v1", + "domain": "echo.executable-subject/v1", + "digest": "sha256:8e2bc64d4c98983c174df8e76c39d74869acb5713c59bc3c2a2af5a9c3083caf" + } + } +} diff --git a/edict/replace-range/edict.executable-subject-lock.json b/edict/replace-range/edict.executable-subject-lock.json new file mode 100644 index 0000000..cba2d22 --- /dev/null +++ b/edict/replace-range/edict.executable-subject-lock.json @@ -0,0 +1,20 @@ +{ + "schema": "jedit.edict-executable-subject-lock/v1", + "reference": { + "coordinate": "echo.executable-subject/v1", + "digest": "sha256:8e2bc64d4c98983c174df8e76c39d74869acb5713c59bc3c2a2af5a9c3083caf" + }, + "rawSha256": "7c7c4695a0029ff6fd45dea942652e2fd7f707464bb9e985ba9b70c4a6d3c849", + "package": { + "coordinate": "executable-operation-package.echo", + "digest": "sha256:6075cb3be2308d7a513a05637499ab130bb35296bbcf14064feb153c47612eb1" + }, + "targetIr": { + "coordinate": "echo.span-ir/v1", + "digest": "sha256:84c1f9826285822f969fc287bc09af2afd63ca876f19f54e5c20ea2e7b424260" + }, + "resultProjection": { + "coordinate": "jedit.text.replace_range@1.replaceRange", + "digest": "sha256:f804baa01c357e5ed54e86f752ea7f8201e454ad0b47c10813fde8c7dc2aeba6" + } +} diff --git a/edict/replace-range/tests/assert-build-output.mjs b/edict/replace-range/tests/assert-build-output.mjs index 4c66cd6..0275764 100644 --- a/edict/replace-range/tests/assert-build-output.mjs +++ b/edict/replace-range/tests/assert-build-output.mjs @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { readFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { decode, encode } from "cbor-x"; @@ -23,8 +23,63 @@ function assertResourceReference(reference, coordinate, digest) { assert.deepEqual(reference.digest, ["sha256", digest]); } -const [outputDirectory] = process.argv.slice(2); +const SOURCE_CLOSURE_PATHS = [ + "edict.application.json", + "edict.lawpack.json", + "edict.toolchain-lock.json", + "src/ReplaceRange.edict", + "tests/assert-build-output.mjs", + "tests/build.sh", + "tests/package-chain.mjs", + "tests/proof-harness.spec.mjs", + "vendor/jedit-text/edict.lawpack-output.json", + "vendor/jedit-text/exports.cbor", + "vendor/jedit-text/exports.sha256", + "vendor/jedit-text/manifest.cbor", + "vendor/jedit-text/manifest.sha256", +]; + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest(); +} + +function lengthPrefix(length) { + const prefix = Buffer.alloc(8); + prefix.writeBigUInt64LE(BigInt(length)); + return prefix; +} + +async function sourceClosure(applicationRoot) { + const hasher = createHash("sha256"); + hasher.update("jedit.edict-source-closure/v1\0"); + const files = []; + for (const relative of SOURCE_CLOSURE_PATHS) { + const bytes = await readFile(path.join(applicationRoot, relative)); + const relativeBytes = Buffer.from(relative, "utf8"); + hasher.update(lengthPrefix(relativeBytes.length)); + hasher.update(relativeBytes); + hasher.update(lengthPrefix(bytes.length)); + hasher.update(bytes); + files.push({ path: relative, sha256: sha256(bytes).toString("hex") }); + } + return { + schema: "jedit.edict-source-closure/v1", + digest: `sha256:${hasher.digest("hex")}`, + files, + }; +} + +function digestText(bytes) { + return `sha256:${Buffer.from(bytes).toString("hex")}`; +} + +const [outputDirectory, mode] = process.argv.slice(2); assert.ok(outputDirectory, "pass the Edict application output directory"); +assert.ok( + mode === undefined || mode === "--write-locks", + "the only supported mode is --write-locks", +); +const applicationRoot = path.resolve(outputDirectory, "../.."); const packageBytes = await readFile( path.join(outputDirectory, "executable-operation-package.cbor"), @@ -72,6 +127,7 @@ const coreDigest = canonicalArtifactDigest( "edict.core.module/v1", program.core_artifact, ); +const coreArtifact = decode(program.core_artifact); assert.deepEqual(executablePackage.semantic_closure.core_identity, coreDigest); assert.deepEqual( executablePackage.semantic_closure.canonical_meaning_identity, @@ -134,3 +190,108 @@ assert.deepEqual( executableSubject.applicationResultProjection, report.applicationResultProjection, ); + +const toolchainLockBytes = await readFile( + path.join(applicationRoot, "edict.toolchain-lock.json"), +); +const toolchainLock = JSON.parse(toolchainLockBytes); +const lawpackReleaseDigest = ( + await readFile( + path.join(applicationRoot, "vendor", "jedit-text", "manifest.sha256"), + "utf8", + ) +).trim(); +const reportDigest = canonicalArtifactDigest( + "echo.operation-package-verifier-report/v1", + reportBytes, +); +const computedBuildLock = { + schema: "jedit.edict-build-lock/v1", + applicationCoordinate: coreArtifact.coordinate, + operationCoordinate: executablePackage.operation_coordinate, + toolchainLockSha256: sha256(toolchainLockBytes).toString("hex"), + sourceClosure: await sourceClosure(applicationRoot), + lawpackRelease: { + coordinate: executablePackage.semantic_closure.lawpack_coordinate, + digest: lawpackReleaseDigest, + }, + provider: { + coordinate: toolchainLock.echo.provider.coordinate, + digest: toolchainLock.echo.provider.digest, + }, + artifacts: { + core: { + coordinate: coreArtifact.coordinate, + domain: "edict.core.module/v1", + digest: digestText(coreDigest), + }, + targetIr: { + coordinate: report.targetIr.id, + domain: "edict.target-ir.artifact/v1", + digest: digestText(targetIrDigest), + }, + resultProjection: { + coordinate: report.applicationResultProjection.id, + domain: "edict.result-projection.artifact/v1", + digest: digestText(resultProjectionDigest), + }, + executablePackage: { + coordinate: report.package.id, + domain: "echo.operation-package/v1", + digest: digestText(packageDigest), + rawSha256: sha256(packageBytes).toString("hex"), + }, + verificationReport: { + coordinate: "verifier-report.echo-operation", + domain: "echo.operation-package-verifier-report/v1", + digest: digestText(reportDigest), + rawSha256: sha256(reportBytes).toString("hex"), + }, + executableSubject: { + coordinate: report.executableSubject.reference.id, + domain: "echo.executable-subject/v1", + digest: digestText(executableSubjectDigest), + }, + }, +}; +const computedSubjectLock = { + schema: "jedit.edict-executable-subject-lock/v1", + reference: { + coordinate: report.executableSubject.reference.id, + digest: digestText(executableSubjectDigest), + }, + rawSha256: sha256(report.executableSubject.bytes).toString("hex"), + package: { + coordinate: report.package.id, + digest: digestText(packageDigest), + }, + targetIr: { + coordinate: report.targetIr.id, + digest: digestText(targetIrDigest), + }, + resultProjection: { + coordinate: report.applicationResultProjection.id, + digest: digestText(resultProjectionDigest), + }, +}; + +const buildLockPath = path.join(applicationRoot, "edict.build-lock.json"); +const subjectLockPath = path.join( + applicationRoot, + "edict.executable-subject-lock.json", +); +if (mode === "--write-locks") { + await writeFile(buildLockPath, `${JSON.stringify(computedBuildLock, null, 2)}\n`); + await writeFile(subjectLockPath, `${JSON.stringify(computedSubjectLock, null, 2)}\n`); +} else { + assert.deepEqual( + JSON.parse(await readFile(buildLockPath, "utf8")), + computedBuildLock, + "the exact build closure differs from edict.build-lock.json", + ); + assert.deepEqual( + JSON.parse(await readFile(subjectLockPath, "utf8")), + computedSubjectLock, + "the executable subject differs from edict.executable-subject-lock.json", + ); +} diff --git a/edict/replace-range/tests/proof-harness.spec.mjs b/edict/replace-range/tests/proof-harness.spec.mjs index b3fa1e4..8ac3b7e 100644 --- a/edict/replace-range/tests/proof-harness.spec.mjs +++ b/edict/replace-range/tests/proof-harness.spec.mjs @@ -193,3 +193,41 @@ test("rejects_short_buffer_and_head_identities", { timeout: 120_000 }, async () await subject.dispose(); } }); + +test("rejects_drifted_build_and_executable_subject_locks", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const lockPath = path.join(subject.application, "edict.build-lock.json"); + const lock = JSON.parse(await readFile(lockPath, "utf8")); + lock.artifacts.core.digest = `sha256:${"0".repeat(64)}`; + await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + + const build = runBuild(subject.project); + assertCommandCompleted(build); + assert.notEqual( + build.status, + 0, + "a changed exact-artifact expectation must fail the package-chain gate", + ); + } finally { + await subject.dispose(); + } +}); + +test("required_ci_executes_the_exact_package_chain", async () => { + const workflow = await readFile( + path.join(projectRoot, ".github", "workflows", "ci.yml"), + "utf8", + ); + assert.match(workflow, /^ edict-replace-range:\n/m); + assert.match(workflow, /name: edict \/ replace-range package chain/); + assert.match( + workflow, + /node --test edict\/replace-range\/tests\/proof-harness\.spec\.mjs/, + ); + assert.match( + workflow, + /needs: \[ plan, build, test-shards, quality, release-gate, edict-replace-range \]/, + ); +}); From a6673521699259abdd27be10f7c885b5c634a867 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 23 Aug 2026 19:50:47 -0700 Subject: [PATCH 9/9] Harden exact Edict package identity proof --- .github/workflows/ci.yml | 10 +- edict/replace-range/README.md | 30 ++- edict/replace-range/edict.build-lock.json | 94 +++++--- .../edict.executable-subject-lock.json | 8 +- edict/replace-range/edict.lawpack.json | 4 +- edict/replace-range/edict.toolchain-lock.json | 43 +++- edict/replace-range/src/ReplaceRange.edict | 2 +- .../tests/assert-build-output.mjs | 93 ++++---- edict/replace-range/tests/package-chain.mjs | 85 ++++++- .../tests/proof-harness.spec.mjs | 209 ++++++++++++++++-- .../tests/verification-evidence.mjs | 83 +++++++ .../jedit-text/edict.lawpack-output.json | 8 +- .../vendor/jedit-text/exports.cbor | Bin 2342 -> 2362 bytes .../vendor/jedit-text/exports.sha256 | 2 +- .../vendor/jedit-text/manifest.cbor | Bin 787 -> 787 bytes .../vendor/jedit-text/manifest.sha256 | 2 +- 16 files changed, 538 insertions(+), 135 deletions(-) create mode 100644 edict/replace-range/tests/verification-evidence.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09b79e4..d90ea2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,8 +177,15 @@ jobs: - name: Checkout Jedit uses: actions/checkout@v6.0.2 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} path: jedit + - name: Verify literal submitted head + working-directory: jedit + env: + EXPECTED_JEDIT_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_JEDIT_HEAD" + - name: Read pinned toolchain commits id: toolchain working-directory: jedit @@ -191,6 +198,7 @@ jobs: console.log(`edict_commit=${lock.edict.commit}`); console.log(`echo_commit=${lock.echo.commit}`); console.log(`rust_toolchain=${lock.rust.toolchain}`); + console.log(`node_version=${lock.validation.nodeVersion}`); NODE - name: Checkout pinned Edict @@ -210,7 +218,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6.4.0 with: - node-version: 22 + node-version: ${{ steps.toolchain.outputs.node_version }} cache: 'npm' cache-dependency-path: jedit/package-lock.json diff --git a/edict/replace-range/README.md b/edict/replace-range/README.md index a063d07..da488a0 100644 --- a/edict/replace-range/README.md +++ b/edict/replace-range/README.md @@ -49,16 +49,24 @@ ECHO_REPO=/path/to/echo \ `edict.toolchain-lock.json` pins the exact Edict commit and CLI release, Rust toolchain, Echo commit, provider identity, provider manifest bytes, and lowerer -and verifier components. The script refuses non-Git roots, wrong commits, or -dirty checkouts before invoking either toolchain. +and verifier components. It also pins the Node and npm releases, the resolved +`cbor-x` package and integrity, and the package, lockfile, and CI invocation +bytes that perform validation. The script refuses non-Git roots, wrong commits, +dirty checkouts, or validation-environment drift before invoking either +toolchain. GitHub's package-chain job checks out and verifies the literal pull +request head; ordinary matrix jobs may separately exercise the synthetic merge. `edict.build-lock.json` binds the exact source and validation closure to the Core, Target IR, result projection, executable package, verification report, -and executable-subject identities. `edict.executable-subject-lock.json` -separately records the subject identity and the exact package, Target IR, and -result-projection references it contains. The harness recomputes every digest -from emitted bytes and refuses a valid report paired with a substituted package. -The locks contain no self-referential Jedit commit; an exact PR head remains an +and executable-subject identities. The raw Echo report remains a separately +identified artifact. A canonical-JSON Jedit evidence envelope binds that artifact +and subject to the exact provider release, verifier component, diagnostic ABI, +report ABI, and outcome; that verifier-specific envelope is the transitional +`VerificationReportId`. `edict.executable-subject-lock.json` separately records +the subject identity and the exact package, Target IR, and result-projection +references it contains. The harness recomputes every digest from emitted bytes +and refuses a valid report paired with a substituted package or verifier. The +locks contain no self-referential Jedit commit; an exact PR head remains an external review coordinate. The script verifies the committed `jedit.text@1` closure through Edict's public @@ -66,8 +74,12 @@ lawpack `checkOnly` boundary. It never republishes or repairs that authoritative tree. It then copies Echo's checked provider package into `.build/`, invokes Edict's public application build, and requires the generic pure package and an accepted independent-verifier report. Snapshots cover the application inputs -outside `.build/` plus every tracked Edict and Echo file; any content, identity, -or timestamp mutation fails the run. Only `.build/` is disposable output. +outside `.build/`, the pinned project validation files, and every tracked Edict +and Echo file; any content, identity, or timestamp mutation fails the run. Only +`.build/` is disposable output. Distinct nominal `BufferId` and `HeadId` +contracts retain the same exact 32-byte representation, while a negative +compiler witness proves that neither can cross the imported lawpack boundary as +the other. The gate must advance again when Echo implements generic pure evaluation; package acceptance is not a permanent substitute for runtime evidence. diff --git a/edict/replace-range/edict.build-lock.json b/edict/replace-range/edict.build-lock.json index 75296cc..63a1834 100644 --- a/edict/replace-range/edict.build-lock.json +++ b/edict/replace-range/edict.build-lock.json @@ -2,83 +2,99 @@ "schema": "jedit.edict-build-lock/v1", "applicationCoordinate": "jedit.text.replace_range@1", "operationCoordinate": "jedit.text.replace_range@1.replaceRange", - "toolchainLockSha256": "69c68a095278d10bc2215afc729dabcba937027da99938862cfb555b8bac9a93", + "toolchainLockSha256": "3396bb91bea7e51d1e4b9f7385421d865d9e5a8f1c7ee1eaf330592143925fa0", "sourceClosure": { "schema": "jedit.edict-source-closure/v1", - "digest": "sha256:dfb61b22dba5616ed3259e49744534ea878aec6b053535c6793cd43e88e26a78", + "digest": "sha256:b6e247a612dd30b62db8b72d5063cbc29c762ffa91f8462fe57b1aadbdda521b", "files": [ { - "path": "edict.application.json", + "path": "application/edict.application.json", "sha256": "3182577627cd1f88bf590f3a8481ef53476c83d783aa142182e7c65eb1f72f8b" }, { - "path": "edict.lawpack.json", - "sha256": "37e91e54ae54ed6852bcc736d0e49e93379321f55b77750b31b713bbb7f0e9d3" + "path": "application/edict.lawpack.json", + "sha256": "150abd5971b40ed70d2b1a1765a1a690e120ad9e44239dae732dc54e6ae04f99" }, { - "path": "edict.toolchain-lock.json", - "sha256": "69c68a095278d10bc2215afc729dabcba937027da99938862cfb555b8bac9a93" + "path": "application/edict.toolchain-lock.json", + "sha256": "3396bb91bea7e51d1e4b9f7385421d865d9e5a8f1c7ee1eaf330592143925fa0" }, { - "path": "src/ReplaceRange.edict", - "sha256": "7af4ee51415bb4391510c5e91a029838a425515abe172aad9c6805b2d70dd916" + "path": "application/src/ReplaceRange.edict", + "sha256": "51797e77062585a9f59083ee8a1da3faaf486ef7691e1b62203369a9d9356ee0" }, { - "path": "tests/assert-build-output.mjs", - "sha256": "a0e48f69ff7828fad421864bcf10ccfe830fa57264b2a95c25b979b0988d1235" + "path": "application/tests/assert-build-output.mjs", + "sha256": "cc54e77a7a5923bb6496817b4822dfd38d5077501f6e12ab55129cf8cfcc86d9" }, { - "path": "tests/build.sh", + "path": "application/tests/build.sh", "sha256": "b5827a1e2a4b64b214406ee3d0cdc9499059922f9878649946c0358fbbd6ea6d" }, { - "path": "tests/package-chain.mjs", - "sha256": "3128f12beba257df3231681fcfc672e4fa4cb4436806e3d410ae577180e8afcf" + "path": "application/tests/package-chain.mjs", + "sha256": "28615de79aa645fa7c3a9cd098865e02af666fa86f49b251d5fe6c919329d141" }, { - "path": "tests/proof-harness.spec.mjs", - "sha256": "541b5599988ea66f85a807c7f249e7db488f7932ccf3765b94e7d12b1dc6c174" + "path": "application/tests/proof-harness.spec.mjs", + "sha256": "a60012f744cff26095a451fe74f984b41a48551e8b47d6f2e5d8e128e1dcd643" }, { - "path": "vendor/jedit-text/edict.lawpack-output.json", - "sha256": "2471666ac595570ea6f812aa613916e20d6f40fd0a99fafb35002be3f4fbcd74" + "path": "application/tests/verification-evidence.mjs", + "sha256": "3022f86f5c96d61f32ced3bec211d08ef7bbcdbc6e772cc61e2019eba0e42218" }, { - "path": "vendor/jedit-text/exports.cbor", - "sha256": "dfee735fd0a7f714a401aa310973596e959672a11bcac5c7c1d289c9344a7a0d" + "path": "application/vendor/jedit-text/edict.lawpack-output.json", + "sha256": "141c0000932ba19cff687e4173e40b0a38410f70bd511c9bc51f3fb3f73ee62b" }, { - "path": "vendor/jedit-text/exports.sha256", - "sha256": "3661e5416b155f803fa0ba888397e4f9b5c79b228ecda538b37b375d3a09791e" + "path": "application/vendor/jedit-text/exports.cbor", + "sha256": "cab9c575da49cb33289e45fc45afb9d7f05558dada734be7eb3acfbbabb1d4db" }, { - "path": "vendor/jedit-text/manifest.cbor", - "sha256": "dadfd41260b1e3449b9fc8c1e5f4c9523b6b819f36fcc1316b53b7aa943a7118" + "path": "application/vendor/jedit-text/exports.sha256", + "sha256": "9bb11d9f8153c18af53b484526d9980b05c27eb20aa97e7623d1bf1ed5a69aea" }, { - "path": "vendor/jedit-text/manifest.sha256", - "sha256": "0496d95c6a0c37e0395e20f7c5c063c0277b92353412b262e935d17d1141d31b" + "path": "application/vendor/jedit-text/manifest.cbor", + "sha256": "0323ed8e9510e473d329166d421dc7d8e1da353065c1cd2dee77e6313ba9bf91" + }, + { + "path": "application/vendor/jedit-text/manifest.sha256", + "sha256": "316cf602502a45cbab89b77b6344854edee750eaf1f0c8167da48d19ead75c22" + }, + { + "path": "project/.github/workflows/ci.yml", + "sha256": "d4100140562e160bd897f9af9fc5d4ee8d7ea9285ba5d0734c620a38cf2d2ea5" + }, + { + "path": "project/package.json", + "sha256": "735a70d8e48f118695529446780d547f8f200dcb0fa453099aa2241d820d9eb5" + }, + { + "path": "project/package-lock.json", + "sha256": "d39f4b39e262e55355315e0a38ad2d10953ae5f6cd48b9fe3a530a128afd01d2" } ] }, "lawpackRelease": { "coordinate": "jedit.text@1", - "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" + "digest": "sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" }, "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:a4e0d9e31ada5c8c14adfce05fdecb228e5b79545f3574a5e21e7db71ab3f307" + "digest": "sha256:b065242a81179df02cd450763c8266e866bbca959308f5198b791a919050e0c2" }, "artifacts": { "core": { "coordinate": "jedit.text.replace_range@1", "domain": "edict.core.module/v1", - "digest": "sha256:a2cec435e178b0f772508125a9c7c6a7a79adc05367cc37955e4a6aa7714c9ef" + "digest": "sha256:d965643ff78c4f321af538f086329fee53c22b21dc1de9e5f9914d6cb69e609f" }, "targetIr": { "coordinate": "echo.span-ir/v1", "domain": "edict.target-ir.artifact/v1", - "digest": "sha256:84c1f9826285822f969fc287bc09af2afd63ca876f19f54e5c20ea2e7b424260" + "digest": "sha256:8a5243fef0aea029df1b6a56eb65f199703e10143d5e0cec5b73603455284229" }, "resultProjection": { "coordinate": "jedit.text.replace_range@1.replaceRange", @@ -88,19 +104,25 @@ "executablePackage": { "coordinate": "executable-operation-package.echo", "domain": "echo.operation-package/v1", - "digest": "sha256:6075cb3be2308d7a513a05637499ab130bb35296bbcf14064feb153c47612eb1", - "rawSha256": "5666555277c9f33a5c2bc3ec5f6859c8ee4005075bc1d6a457f12a384065a8f3" + "digest": "sha256:9a9e26e10dabf9175889fb2ef50bd463dd08ea95e9a377e0a39eb4663acd409b", + "rawSha256": "f339cf8e52621df4a9b644c83770932df5c01c52e70d1bd271c0e6519cda2998" }, - "verificationReport": { + "verificationReportArtifact": { "coordinate": "verifier-report.echo-operation", "domain": "echo.operation-package-verifier-report/v1", - "digest": "sha256:410e10dfa931c57da3aaf64a50ef4b62a63217e97e6152cdc2a22a661685bfd9", - "rawSha256": "4038b28354fe8555a581fe6e87fec7edcd550e27bbe54693f997558b86da1a97" + "digest": "sha256:c8f1e52216f248022a884c822d577c479b3a68b0f5ad69f21db3288a00e474a1", + "rawSha256": "124cfca3449d19684a169a4fe62898dfb76124854d0f0ef57ea3ed9f606a4ce0" + }, + "verificationReport": { + "coordinate": "jedit.edict-verification-evidence/v1", + "domain": "jedit.edict-verification-evidence/v1", + "digest": "sha256:54f4bc09addabf3236478c891aa8ced4f13a2738e007f95c96f547b6b36dcd80", + "rawSha256": "6a6420061353e34a8a8db21025b9eeed7a0802c720b4e865e8f6218448f5be4e" }, "executableSubject": { "coordinate": "echo.executable-subject/v1", "domain": "echo.executable-subject/v1", - "digest": "sha256:8e2bc64d4c98983c174df8e76c39d74869acb5713c59bc3c2a2af5a9c3083caf" + "digest": "sha256:9738fc187d8507b64bc7c01a536c8eb2b99fb550380db878d49ebb7e130267aa" } } } diff --git a/edict/replace-range/edict.executable-subject-lock.json b/edict/replace-range/edict.executable-subject-lock.json index cba2d22..d0605bb 100644 --- a/edict/replace-range/edict.executable-subject-lock.json +++ b/edict/replace-range/edict.executable-subject-lock.json @@ -2,16 +2,16 @@ "schema": "jedit.edict-executable-subject-lock/v1", "reference": { "coordinate": "echo.executable-subject/v1", - "digest": "sha256:8e2bc64d4c98983c174df8e76c39d74869acb5713c59bc3c2a2af5a9c3083caf" + "digest": "sha256:9738fc187d8507b64bc7c01a536c8eb2b99fb550380db878d49ebb7e130267aa" }, - "rawSha256": "7c7c4695a0029ff6fd45dea942652e2fd7f707464bb9e985ba9b70c4a6d3c849", + "rawSha256": "46255257cc23601a10f2dd1fd5d7bc86fc4bb13612eb2b3ac5f633ded159aa88", "package": { "coordinate": "executable-operation-package.echo", - "digest": "sha256:6075cb3be2308d7a513a05637499ab130bb35296bbcf14064feb153c47612eb1" + "digest": "sha256:9a9e26e10dabf9175889fb2ef50bd463dd08ea95e9a377e0a39eb4663acd409b" }, "targetIr": { "coordinate": "echo.span-ir/v1", - "digest": "sha256:84c1f9826285822f969fc287bc09af2afd63ca876f19f54e5c20ea2e7b424260" + "digest": "sha256:8a5243fef0aea029df1b6a56eb65f199703e10143d5e0cec5b73603455284229" }, "resultProjection": { "coordinate": "jedit.text.replace_range@1.replaceRange", diff --git a/edict/replace-range/edict.lawpack.json b/edict/replace-range/edict.lawpack.json index 13483ef..2652d16 100644 --- a/edict/replace-range/edict.lawpack.json +++ b/edict/replace-range/edict.lawpack.json @@ -18,11 +18,11 @@ }, { "coordinate": "jedit.text@1.BufferId", - "definition": "jedit.text@1.NodeId" + "definition": "Nominal" }, { "coordinate": "jedit.text@1.HeadId", - "definition": "jedit.text@1.NodeId" + "definition": "Nominal" }, { "coordinate": "jedit.text@1.ReplacementBytes", diff --git a/edict/replace-range/edict.toolchain-lock.json b/edict/replace-range/edict.toolchain-lock.json index a0369e4..110a908 100644 --- a/edict/replace-range/edict.toolchain-lock.json +++ b/edict/replace-range/edict.toolchain-lock.json @@ -5,23 +5,54 @@ "rustcVersion": "rustc 1.94.0 (4a4ef493e 2026-03-02)", "cargoVersion": "cargo 1.94.0 (85eff7c80 2026-01-15)" }, + "validation": { + "nodeVersion": "22.23.1", + "processVersion": "v22.23.1", + "npmVersion": "10.9.8", + "cborX": { + "version": "1.6.4", + "integrity": "sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==" + }, + "files": [ + { + "path": "package.json", + "sha256": "735a70d8e48f118695529446780d547f8f200dcb0fa453099aa2241d820d9eb5" + }, + { + "path": "package-lock.json", + "sha256": "d39f4b39e262e55355315e0a38ad2d10953ae5f6cd48b9fe3a530a128afd01d2" + }, + { + "path": ".github/workflows/ci.yml", + "sha256": "d4100140562e160bd897f9af9fc5d4ee8d7ea9285ba5d0734c620a38cf2d2ea5" + } + ] + }, "edict": { - "commit": "68633c409474b9dffac4a33a674b2a3f29ad865c", + "commit": "39a796de04b3400f569880da06878da50d8ed0ee", "cliVersion": "0.11.0-alpha.1" }, "echo": { - "commit": "27a56eb91964aeeea90d36c58585683b341a2dfc", + "commit": "49e9efb68001dfd78563d18bac9359a87671e431", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:a4e0d9e31ada5c8c14adfce05fdecb228e5b79545f3574a5e21e7db71ab3f307", - "manifestSha256": "205bdba71fd552316be59079d7509c592b1ac98af7197a119fe56ce12ea0a5f6", + "digest": "sha256:b065242a81179df02cd450763c8266e866bbca959308f5198b791a919050e0c2", + "manifestSha256": "c5b9fb2fe3a0dc4dad282621a97413225c555be0071f3502b3272952069d42dc", + "targetProfile": { + "coordinate": "echo.dpo@1", + "digest": "sha256:2e2494121aecf5e6a2d920f5fb85408825d394765fad41484c416397c920fb04" + }, "lowerer": { "coordinate": "echo.dpo.lowerer/component@1", - "sha256": "a4758f060122fba8c073841ccaa22bf0b8a742b495b76eb33c914eff3250dcf5" + "sha256": "e842063bd8d8ec12fff5b392a9d6bc646e2e3025c7a0c93726f53eed3cb5a0bf" }, "verifier": { "coordinate": "echo.dpo.verifier/component@1", - "sha256": "174cf8758815bf2b9f2ef575517aa6d144ed3f2d1995fc65d25a211b9dea82d9" + "sha256": "174cf8758815bf2b9f2ef575517aa6d144ed3f2d1995fc65d25a211b9dea82d9", + "contract": { + "coordinate": "echo.dpo.verifier/v1", + "digest": "sha256:ca96b190728de3d668072ec1bd37d24e5197e7bd9bd54f70966d8c566b9b67f2" + } } } } diff --git a/edict/replace-range/src/ReplaceRange.edict b/edict/replace-range/src/ReplaceRange.edict index 0c77483..6079dbe 100644 --- a/edict/replace-range/src/ReplaceRange.edict +++ b/edict/replace-range/src/ReplaceRange.edict @@ -1,6 +1,6 @@ package jedit.text.replace_range@1; -use lawpack jedit.text@1 digest "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" as text; +use lawpack jedit.text@1 digest "sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" as text; type ReplaceRangeInput = { bufferId: text.BufferId, diff --git a/edict/replace-range/tests/assert-build-output.mjs b/edict/replace-range/tests/assert-build-output.mjs index 0275764..fc0d14a 100644 --- a/edict/replace-range/tests/assert-build-output.mjs +++ b/edict/replace-range/tests/assert-build-output.mjs @@ -6,37 +6,49 @@ import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { decode, encode } from "cbor-x"; +import { decode } from "cbor-x"; -function canonicalArtifactDigest(domain, canonicalArtifactBytes) { - const preimage = Buffer.concat([ - Buffer.from([0x83]), - Buffer.from(encode("edict.digest/v1")), - Buffer.from(encode(domain)), - Buffer.from(canonicalArtifactBytes), - ]); - return createHash("sha256").update(preimage).digest(); -} +import { + canonicalArtifactDigest, + digestText, + verificationEvidence, +} from "./verification-evidence.mjs"; function assertResourceReference(reference, coordinate, digest) { assert.equal(reference.id, coordinate); assert.deepEqual(reference.digest, ["sha256", digest]); } -const SOURCE_CLOSURE_PATHS = [ - "edict.application.json", - "edict.lawpack.json", - "edict.toolchain-lock.json", - "src/ReplaceRange.edict", - "tests/assert-build-output.mjs", - "tests/build.sh", - "tests/package-chain.mjs", - "tests/proof-harness.spec.mjs", - "vendor/jedit-text/edict.lawpack-output.json", - "vendor/jedit-text/exports.cbor", - "vendor/jedit-text/exports.sha256", - "vendor/jedit-text/manifest.cbor", - "vendor/jedit-text/manifest.sha256", +const SOURCE_CLOSURE_INPUTS = [ + ["application/edict.application.json", "edict.application.json"], + ["application/edict.lawpack.json", "edict.lawpack.json"], + ["application/edict.toolchain-lock.json", "edict.toolchain-lock.json"], + ["application/src/ReplaceRange.edict", "src/ReplaceRange.edict"], + ["application/tests/assert-build-output.mjs", "tests/assert-build-output.mjs"], + ["application/tests/build.sh", "tests/build.sh"], + ["application/tests/package-chain.mjs", "tests/package-chain.mjs"], + ["application/tests/proof-harness.spec.mjs", "tests/proof-harness.spec.mjs"], + [ + "application/tests/verification-evidence.mjs", + "tests/verification-evidence.mjs", + ], + [ + "application/vendor/jedit-text/edict.lawpack-output.json", + "vendor/jedit-text/edict.lawpack-output.json", + ], + ["application/vendor/jedit-text/exports.cbor", "vendor/jedit-text/exports.cbor"], + [ + "application/vendor/jedit-text/exports.sha256", + "vendor/jedit-text/exports.sha256", + ], + ["application/vendor/jedit-text/manifest.cbor", "vendor/jedit-text/manifest.cbor"], + [ + "application/vendor/jedit-text/manifest.sha256", + "vendor/jedit-text/manifest.sha256", + ], + ["project/.github/workflows/ci.yml", "../../.github/workflows/ci.yml"], + ["project/package.json", "../../package.json"], + ["project/package-lock.json", "../../package-lock.json"], ]; function sha256(bytes) { @@ -53,14 +65,14 @@ async function sourceClosure(applicationRoot) { const hasher = createHash("sha256"); hasher.update("jedit.edict-source-closure/v1\0"); const files = []; - for (const relative of SOURCE_CLOSURE_PATHS) { + for (const [identityPath, relative] of SOURCE_CLOSURE_INPUTS) { const bytes = await readFile(path.join(applicationRoot, relative)); - const relativeBytes = Buffer.from(relative, "utf8"); + const relativeBytes = Buffer.from(identityPath, "utf8"); hasher.update(lengthPrefix(relativeBytes.length)); hasher.update(relativeBytes); hasher.update(lengthPrefix(bytes.length)); hasher.update(bytes); - files.push({ path: relative, sha256: sha256(bytes).toString("hex") }); + files.push({ path: identityPath, sha256: sha256(bytes).toString("hex") }); } return { schema: "jedit.edict-source-closure/v1", @@ -69,10 +81,6 @@ async function sourceClosure(applicationRoot) { }; } -function digestText(bytes) { - return `sha256:${Buffer.from(bytes).toString("hex")}`; -} - const [outputDirectory, mode] = process.argv.slice(2); assert.ok(outputDirectory, "pass the Edict application output directory"); assert.ok( @@ -205,6 +213,21 @@ const reportDigest = canonicalArtifactDigest( "echo.operation-package-verifier-report/v1", reportBytes, ); +const reportArtifactId = { + coordinate: "verifier-report.echo-operation", + domain: "echo.operation-package-verifier-report/v1", + digest: digestText(reportDigest), + rawSha256: sha256(reportBytes).toString("hex"), +}; +const evidence = verificationEvidence({ + executableSubjectId: { + coordinate: report.executableSubject.reference.id, + digest: digestText(executableSubjectDigest), + }, + provider: toolchainLock.echo.provider, + reportArtifactId, + report, +}); const computedBuildLock = { schema: "jedit.edict-build-lock/v1", applicationCoordinate: coreArtifact.coordinate, @@ -241,12 +264,8 @@ const computedBuildLock = { digest: digestText(packageDigest), rawSha256: sha256(packageBytes).toString("hex"), }, - verificationReport: { - coordinate: "verifier-report.echo-operation", - domain: "echo.operation-package-verifier-report/v1", - digest: digestText(reportDigest), - rawSha256: sha256(reportBytes).toString("hex"), - }, + verificationReportArtifact: reportArtifactId, + verificationReport: evidence.identity, executableSubject: { coordinate: report.executableSubject.reference.id, domain: "echo.executable-subject/v1", diff --git a/edict/replace-range/tests/package-chain.mjs b/edict/replace-range/tests/package-chain.mjs index 3133466..b521798 100644 --- a/edict/replace-range/tests/package-chain.mjs +++ b/edict/replace-range/tests/package-chain.mjs @@ -17,6 +17,8 @@ import { import path from "node:path"; import { fileURLToPath } from "node:url"; +import { decode } from "cbor-x"; + const testsDirectory = path.dirname(fileURLToPath(import.meta.url)); const applicationRoot = path.resolve(testsDirectory, ".."); const projectRoot = path.resolve(applicationRoot, "../.."); @@ -98,8 +100,35 @@ async function verifyProvider(lock) { const artifact = providerArtifact(manifest, role); assert.equal(artifact.resource.coordinate, expected.coordinate); assert.equal(artifact.resource.digest, `sha256:${expected.sha256}`); - assert.equal(await sha256(path.join(providerRoot, "components", fileName)), expected.sha256); + assert.equal( + await sha256(path.join(providerRoot, "components", fileName)), + expected.sha256, + ); } + const targetProfileArtifact = providerArtifact( + manifest, + "target-profile.echo-dpo", + ); + assert.equal( + targetProfileArtifact.resource.coordinate, + lock.targetProfile.coordinate, + ); + assert.equal(targetProfileArtifact.resource.digest, lock.targetProfile.digest); + const targetProfile = decode( + await readFile( + path.join( + providerRoot, + "generated", + "primary", + "target-profile.echo-dpo.cbor", + ), + ), + ); + assert.equal(targetProfile.verifier.id, lock.verifier.contract.coordinate); + assert.equal( + `sha256:${Buffer.from(targetProfile.verifier.digest[1]).toString("hex")}`, + lock.verifier.contract.digest, + ); return providerRoot; } @@ -156,9 +185,14 @@ async function snapshotFile(namespace, root, relative) { return { ...common, kind: "other" }; } -async function snapshotAuthoritativeInputs() { +async function snapshotAuthoritativeInputs(lock) { const roots = [ ["jedit", applicationRoot, (await collectDirectoryFiles(applicationRoot)).sort()], + [ + "jedit-project", + projectRoot, + lock.validation.files.map((file) => file.path).sort(), + ], ["edict", edictRepository, trackedFiles(edictRepository)], ["echo", echoRepository, trackedFiles(echoRepository)], ]; @@ -172,8 +206,34 @@ async function snapshotAuthoritativeInputs() { } async function verifyVersions(lock) { - assert.equal(run("rustc", [`+${lock.rust.toolchain}`, "--version"]), lock.rust.rustcVersion); - assert.equal(run("cargo", [`+${lock.rust.toolchain}`, "--version"]), lock.rust.cargoVersion); + assert.equal(process.version, lock.validation.processVersion); + assert.equal(run("npm", ["--version"]), lock.validation.npmVersion); + const packageLock = JSON.parse( + await readFile(path.join(projectRoot, "package-lock.json")), + ); + assert.equal( + packageLock.packages["node_modules/cbor-x"].version, + lock.validation.cborX.version, + ); + assert.equal( + packageLock.packages["node_modules/cbor-x"].integrity, + lock.validation.cborX.integrity, + ); + for (const file of lock.validation.files) { + assert.equal( + await sha256(path.join(projectRoot, file.path)), + file.sha256, + `${file.path} differs from the validation-environment lock`, + ); + } + assert.equal( + run("rustc", [`+${lock.rust.toolchain}`, "--version"]), + lock.rust.rustcVersion, + ); + assert.equal( + run("cargo", [`+${lock.rust.toolchain}`, "--version"]), + lock.rust.cargoVersion, + ); run( "cargo", [`+${lock.rust.toolchain}`, "build", "--locked", "-p", "edict-cli"], @@ -191,7 +251,7 @@ async function build() { await verifyGitCheckout(edictRepository, lock.edict.commit, "Edict"); await verifyGitCheckout(echoRepository, lock.echo.commit, "Echo"); const providerSource = await verifyProvider(lock.echo.provider); - const before = await snapshotAuthoritativeInputs(); + const before = await snapshotAuthoritativeInputs(lock); let failure; try { const edictBinary = await verifyVersions(lock); @@ -224,14 +284,16 @@ async function build() { application: "edict.application.json", })}\n`, }); - run(process.execPath, [path.join(testsDirectory, "assert-build-output.mjs"), applicationOutput], { - cwd: applicationRoot, - }); + run( + process.execPath, + [path.join(testsDirectory, "assert-build-output.mjs"), applicationOutput], + { cwd: applicationRoot }, + ); } catch (error) { failure = error; } - const after = await snapshotAuthoritativeInputs(); + const after = await snapshotAuthoritativeInputs(lock); let mutationFailure; try { assert.deepEqual(after, before, "authoritative inputs changed during package-chain build"); @@ -239,7 +301,10 @@ async function build() { mutationFailure = error; } if (failure && mutationFailure) { - throw new AggregateError([failure, mutationFailure], "build failed and authoritative inputs changed"); + throw new AggregateError( + [failure, mutationFailure], + "build failed and authoritative inputs changed", + ); } if (mutationFailure) { throw mutationFailure; diff --git a/edict/replace-range/tests/proof-harness.spec.mjs b/edict/replace-range/tests/proof-harness.spec.mjs index 8ac3b7e..c860a4c 100644 --- a/edict/replace-range/tests/proof-harness.spec.mjs +++ b/edict/replace-range/tests/proof-harness.spec.mjs @@ -11,6 +11,8 @@ import { fileURLToPath } from "node:url"; import { decode, encode } from "cbor-x"; +import { verificationEvidence } from "./verification-evidence.mjs"; + const testDirectory = path.dirname(fileURLToPath(import.meta.url)); const applicationRoot = path.resolve(testDirectory, ".."); const projectRoot = path.resolve(applicationRoot, "../.."); @@ -28,7 +30,21 @@ async function fixture() { const fixtureApplication = path.join(fixtureProject, "edict", "replace-range"); await mkdir(path.dirname(fixtureApplication), { recursive: true }); await cp(applicationRoot, fixtureApplication, { recursive: true }); - await symlink(path.join(projectRoot, "node_modules"), path.join(fixtureProject, "node_modules"), "dir"); + await mkdir(path.join(fixtureProject, ".github", "workflows"), { + recursive: true, + }); + for (const relative of [ + ".github/workflows/ci.yml", + "package.json", + "package-lock.json", + ]) { + await cp(path.join(projectRoot, relative), path.join(fixtureProject, relative)); + } + await symlink( + path.join(projectRoot, "node_modules"), + path.join(fixtureProject, "node_modules"), + "dir", + ); return { root, project: fixtureProject, @@ -58,7 +74,7 @@ function assertCommandCompleted(result) { assert.notEqual(result.status, null, `command terminated by ${result.signal}`); } -test("rejects_non_git_or_wrong_commit_toolchain_inputs", { timeout: 120_000 }, async () => { +test("rejects_non_git_toolchain_inputs", { timeout: 120_000 }, async () => { requireToolchainEnvironment(); const subject = await fixture(); try { @@ -99,6 +115,23 @@ test("rejects_non_git_or_wrong_commit_toolchain_inputs", { timeout: 120_000 }, a } }); +test("rejects_wrong_commit_toolchain_inputs", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const lockPath = path.join(subject.application, "edict.toolchain-lock.json"); + const lock = JSON.parse(await readFile(lockPath, "utf8")); + lock.edict.commit = "0".repeat(40); + await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + + const result = runBuild(subject.project); + assertCommandCompleted(result); + assert.notEqual(result.status, 0, "a wrong Edict commit must be rejected"); + } finally { + await subject.dispose(); + } +}); + test("lawpack_check_only_rejects_drift_without_repairing_it", { timeout: 120_000 }, async () => { requireToolchainEnvironment(); const subject = await fixture(); @@ -163,37 +196,138 @@ test( }, ); -test("rejects_short_buffer_and_head_identities", { timeout: 120_000 }, async () => { +test( + "preserves_nominal_exact_length_buffer_and_head_identities", + { timeout: 120_000 }, + async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const build = runBuild(subject.project); + assertCommandCompleted(build); + assert.equal(build.status, 0, build.stderr); + + const packageBytes = await readFile( + path.join( + subject.application, + ".build", + "application", + "executable-operation-package.cbor", + ), + ); + const executablePackage = decode(packageBytes); + const program = decode(executablePackage.program); + const core = decode(program.core_artifact); + for (const typeName of [ + "ReplaceRangeInput.bufferId", + "ReplaceRangeInput.basisHeadId", + "ReplaceRangeBoundary.bufferId", + "ReplaceRangeBoundary.basisHeadId", + ]) { + assert.deepEqual( + core.types[typeName], + { + kind: "Nominal", + contract: + typeName.endsWith("bufferId") + ? "jedit.text@1.BufferId" + : "jedit.text@1.HeadId", + representation: "Bytes", + }, + `${typeName} must preserve its nominal exact 32-byte contract in Core`, + ); + } + } finally { + await subject.dispose(); + } + }, +); + +test("rejects_buffer_id_head_id_substitution", { timeout: 120_000 }, async () => { requireToolchainEnvironment(); const subject = await fixture(); try { - const build = runBuild(subject.project); - assertCommandCompleted(build); - assert.equal(build.status, 0, build.stderr); + const sourcePath = path.join( + subject.application, + "src", + "ReplaceRange.edict", + ); + const source = await readFile(sourcePath, "utf8"); + const crossed = source + .replace("bufferId: input.bufferId,", "bufferId: input.basisHeadId,") + .replace("basisHeadId: input.basisHeadId,", "basisHeadId: input.bufferId,"); + assert.notEqual( + crossed, + source, + "the negative witness must cross the two identities", + ); + await writeFile(sourcePath, crossed); - const packageBytes = await readFile( - path.join(subject.application, ".build", "application", "executable-operation-package.cbor"), + const result = runBuild(subject.project); + assertCommandCompleted(result); + assert.notEqual(result.status, 0, "BufferId and HeadId must not be substitutable"); + assert.match( + `${result.stdout}\n${result.stderr}`, + /TypeMismatch/, + "the imported nominal boundary must reject the crossed assignment", ); - const executablePackage = decode(packageBytes); - const program = decode(executablePackage.program); - const core = decode(program.core_artifact); - for (const typeName of [ - "ReplaceRangeInput.bufferId", - "ReplaceRangeInput.basisHeadId", - "ReplaceRangeBoundary.bufferId", - "ReplaceRangeBoundary.basisHeadId", - ]) { - assert.deepEqual( - core.types[typeName], - { kind: "Bytes", min: 32, max: 32 }, - `${typeName} must preserve the exact 32-byte identity contract in Core`, - ); - } } finally { await subject.dispose(); } }); +test("verification_report_identity_is_verifier_specific", () => { + const common = { + executableSubjectId: { + coordinate: "echo.executable-subject/v1", + digest: `sha256:${"1".repeat(64)}`, + }, + reportArtifactId: { + coordinate: "verifier-report.echo-operation", + domain: "echo.operation-package-verifier-report/v1", + digest: `sha256:${"2".repeat(64)}`, + rawSha256: "3".repeat(64), + }, + report: { + apiVersion: "echo.operation-package-verifier-report/v1", + diagnosticAbi: { + id: "edict.diagnostics/v1", + digest: ["sha256", Buffer.alloc(32, 4)], + }, + outcome: "accepted", + }, + }; + const provider = { + coordinate: "echo.edict-provider@1", + digest: `sha256:${"5".repeat(64)}`, + verifier: { + coordinate: "echo.dpo.verifier/component@1", + sha256: "6".repeat(64), + contract: { + coordinate: "echo.dpo.verifier/v1", + digest: `sha256:${"8".repeat(64)}`, + }, + }, + targetProfile: { + coordinate: "echo.dpo@1", + digest: `sha256:${"9".repeat(64)}`, + }, + }; + const original = verificationEvidence({ ...common, provider }); + const changed = verificationEvidence({ + ...common, + provider: { + ...provider, + verifier: { ...provider.verifier, sha256: "7".repeat(64) }, + }, + }); + assert.notEqual( + original.identity.digest, + changed.identity.digest, + "different verifier identities must produce different VerificationReportIds", + ); +}); + test("rejects_drifted_build_and_executable_subject_locks", { timeout: 120_000 }, async () => { requireToolchainEnvironment(); const subject = await fixture(); @@ -215,6 +349,30 @@ test("rejects_drifted_build_and_executable_subject_locks", { timeout: 120_000 }, } }); +test("rejects_drifted_executable_subject_lock", { timeout: 120_000 }, async () => { + requireToolchainEnvironment(); + const subject = await fixture(); + try { + const lockPath = path.join( + subject.application, + "edict.executable-subject-lock.json", + ); + const lock = JSON.parse(await readFile(lockPath, "utf8")); + lock.reference.digest = `sha256:${"0".repeat(64)}`; + await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + + const build = runBuild(subject.project); + assertCommandCompleted(build); + assert.notEqual( + build.status, + 0, + "a changed executable-subject expectation must fail the package-chain gate", + ); + } finally { + await subject.dispose(); + } +}); + test("required_ci_executes_the_exact_package_chain", async () => { const workflow = await readFile( path.join(projectRoot, ".github", "workflows", "ci.yml"), @@ -226,6 +384,11 @@ test("required_ci_executes_the_exact_package_chain", async () => { workflow, /node --test edict\/replace-range\/tests\/proof-harness\.spec\.mjs/, ); + assert.match( + workflow, + /ref: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/, + ); + assert.match(workflow, /git rev-parse HEAD/); assert.match( workflow, /needs: \[ plan, build, test-shards, quality, release-gate, edict-replace-range \]/, diff --git a/edict/replace-range/tests/verification-evidence.mjs b/edict/replace-range/tests/verification-evidence.mjs new file mode 100644 index 0000000..2286e29 --- /dev/null +++ b/edict/replace-range/tests/verification-evidence.mjs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +// © James Ross Ω FLYING•ROBOTS + +import { createHash } from "node:crypto"; + +import { encode } from "cbor-x"; + +export function canonicalArtifactDigest(domain, canonicalArtifactBytes) { + const preimage = Buffer.concat([ + Buffer.from([0x83]), + Buffer.from(encode("edict.digest/v1")), + Buffer.from(encode(domain)), + Buffer.from(canonicalArtifactBytes), + ]); + return createHash("sha256").update(preimage).digest(); +} + +export function digestText(bytes) { + return `sha256:${Buffer.from(bytes).toString("hex")}`; +} + +function canonicalJson(value) { + if (Array.isArray(value)) { + return value.map(canonicalJson); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalJson(value[key])]), + ); + } + return value; +} + +function resourceReference(reference) { + return { + coordinate: reference.id, + digest: digestText(reference.digest[1]), + }; +} + +export function verificationEvidence({ + executableSubjectId, + provider, + reportArtifactId, + report, +}) { + const envelope = { + apiVersion: "jedit.edict-verification-evidence/v1", + reportArtifact: reportArtifactId, + executableSubject: executableSubjectId, + providerRelease: { + coordinate: provider.coordinate, + digest: provider.digest, + }, + verifierComponent: { + coordinate: provider.verifier.coordinate, + digest: `sha256:${provider.verifier.sha256}`, + }, + verificationPolicy: { + apiVersion: "jedit.edict-verification-policy/v1", + verifierContract: provider.verifier.contract, + targetProfile: provider.targetProfile, + reportAbi: report.apiVersion, + diagnosticAbi: resourceReference(report.diagnosticAbi), + }, + outcome: report.outcome, + }; + const bytes = Buffer.from(JSON.stringify(canonicalJson(envelope)), "utf8"); + return { + envelope, + bytes, + identity: { + coordinate: "jedit.edict-verification-evidence/v1", + domain: "jedit.edict-verification-evidence/v1", + digest: digestText( + canonicalArtifactDigest("jedit.edict-verification-evidence/v1", bytes), + ), + rawSha256: createHash("sha256").update(bytes).digest("hex"), + }, + }; +} diff --git a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json index 0c5d525..5907953 100644 --- a/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json +++ b/edict/replace-range/vendor/jedit-text/edict.lawpack-output.json @@ -7,25 +7,25 @@ "path": "manifest.cbor", "kind": "manifest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" + "digest": "sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" }, { "path": "manifest.sha256", "kind": "manifestDigest", "coordinate": "edict.lawpack/v1", - "digest": "sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf" + "digest": "sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" }, { "path": "exports.cbor", "kind": "exports", "coordinate": "jedit.text.exports/v1", - "digest": "sha256:ffa6529b8327ab49e7cf62016e940e5cabf90c5468d9d1efb0dcb3d7d6574c88" + "digest": "sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a" }, { "path": "exports.sha256", "kind": "exportsDigest", "coordinate": "jedit.text.exports/v1", - "digest": "sha256:ffa6529b8327ab49e7cf62016e940e5cabf90c5468d9d1efb0dcb3d7d6574c88" + "digest": "sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a" }, { "path": "echo-operation-configuration.cbor", diff --git a/edict/replace-range/vendor/jedit-text/exports.cbor b/edict/replace-range/vendor/jedit-text/exports.cbor index 65bcda4a8cee4eb7fe8f3ee4237e28d580797495..80925499a5d53b13815dd48e3ed972c3deffd6cb 100644 GIT binary patch delta 50 ucmZ1`v`c700at~LUw&?8USf{T#6n?4yNNB@s6tG3iza6=NpHOG&H(_uA`w{t delta 60 ycmdlbv`lD1fk<&yYD#8_UP)?2iG!h@Uw%reXUfDrZBcCEize4GNpJk@&H(_X_!dh5 diff --git a/edict/replace-range/vendor/jedit-text/exports.sha256 b/edict/replace-range/vendor/jedit-text/exports.sha256 index 9542b30..87a0814 100644 --- a/edict/replace-range/vendor/jedit-text/exports.sha256 +++ b/edict/replace-range/vendor/jedit-text/exports.sha256 @@ -1 +1 @@ -sha256:ffa6529b8327ab49e7cf62016e940e5cabf90c5468d9d1efb0dcb3d7d6574c88 +sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a diff --git a/edict/replace-range/vendor/jedit-text/manifest.cbor b/edict/replace-range/vendor/jedit-text/manifest.cbor index 9049ed68091b5df76093e71bd90ddda5582adf66..7bbc2c452f94541ff049a5d8b88f272775da67f6 100644 GIT binary patch delta 43 zcmV+`0M!4J29pMmLLke$dHs^`pEMdDt|A&LhMTrd6~z&+IWs;4Q_Y9|MQX8Pu>zD; B5=#I8 delta 43 zcmV+`0M!4J29pMmLLmR9Qk#P(t4Zh2VgYWH4qU7G3{+^@(eJR_v)9&FOo*{!u>!v^ B6}$id diff --git a/edict/replace-range/vendor/jedit-text/manifest.sha256 b/edict/replace-range/vendor/jedit-text/manifest.sha256 index b329237..be4e2d8 100644 --- a/edict/replace-range/vendor/jedit-text/manifest.sha256 +++ b/edict/replace-range/vendor/jedit-text/manifest.sha256 @@ -1 +1 @@ -sha256:5253f7c8682cfed88a34c660a610c30da6bd9924973fefd33a82c9915a4f09cf +sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e