From c0903ad48cbcecf71512b7091924f0f2ecc894d3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:09:57 -0700 Subject: [PATCH 1/4] Reconcile canonical ownership with the executable flat-text reference --- docs/jim-component-ownership.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/jim-component-ownership.md b/docs/jim-component-ownership.md index 300236dd..8295f33a 100644 --- a/docs/jim-component-ownership.md +++ b/docs/jim-component-ownership.md @@ -697,13 +697,28 @@ definitions or test oracles accidentally become fake runtime implementations. | Installed release record | Echo | One complete closure was retained, installed, verified, and activated for a worldline under an explicit transition | That a particular command committed | | Tick, conflict outcome, reading, receipt, and recovery evidence | Echo realm | What happened under one release, realm, epoch, basis, and admitted or rejected execution | What Jim should decide next | -The schema and oracle are already frozen under -`contracts/jedit/lawpacks/replace-range-v1/`. `ReplaceRange.edict` and its -complete application-owned lawpack closure do not yet exist in Jedit. Neither -does the release manifest that would bind `jim.core`, `jedit.text`, their entry -points, and the relevant profiles. Edict's generic application-owned lawpack -authoring and bounded-control prerequisites have landed, but only the real -application source can reveal the next honest compiler or target-profile gap. +The schema and independent oracle are frozen under +`contracts/jedit/lawpacks/replace-range-v1/`. Jedit now owns executable source +and its complete published lawpack closure under `edict/replace-range/`. +The public Edict build and independent verifier produce a pinned pure package +that Echo executes as a flat-text replacement reference, with range, UTF-8, +offset-boundary, no-op and resource refusals. The package and verifier report +are byte-identical across the verified macOS and Linux builds. The application +[README](../edict/replace-range/README.md) owns the exact build and runtime +reproduction instructions. + +This reference computes bytes; it does not construct a persistent rope or +produce the domain's typed outcomes. Authored graph effects, atomic admission, +Tick/WAL/receipt/readings and fresh-engine recovery remain unimplemented for +this package. The standalone runtime host is test-only conformance scaffolding, +unreachable from the editor's production composition. The proposed persistent +rope source remains a noncompiling draft outside the application gate. + +The release manifest binding `jim.core`, `jedit.text`, their entry points and +profiles, and the installed release that would activate it, also remain absent. +This working pure package refines the authored-source stage of the artifact +chain; it does not establish an installed Jim release or replace the current +native text-operation authority. In summary, the artifact chain moves from application definition, to authored source and lawpack closure, to compiler-produced package, to installed release, @@ -1178,7 +1193,7 @@ but contradictory prose. | Which TypeScript paths still own semantics? | Vim parsing, motion resolution, coordinate conversion, operation selection, and transition planning are executable today | Classify them explicitly as compatibility authority, not merely import or migration plumbing | | Who owns text and rope contracts in target prose? | Some older documents say “Jedit” broadly, while the first draft grouped them under Jim | Name `jedit.text` as the application-owned text capability, `jim.core` as its semantic consumer, and Jedit as the product/body composition | | Do documentation tests prove the canonical section? | Some policy tests search required phrases anywhere in a document | Scope tests to the named ownership sections and assert the complete contract, including test-only direct `ReplaceRange` invocation | -| Does the target exist in code? | `Jim.edict`, `ReplaceRange.edict`, `TextWindow.edict`, `JimRelease`, authority realms, durable conflict resolution, and materialization guarantee providers are absent or unproven | Keep every target statement labeled as target until compiler, runtime, recovery, and external-effect evidence exists | +| Does the target exist in code? | A compiler-produced pure flat-text `ReplaceRange.edict` reference exists; the authored persistent-rope operation, `Jim.edict`, `TextWindow.edict`, `JimRelease`, authority realms, durable conflict resolution, and materialization guarantee providers remain absent or unproven | Keep every target statement labeled as target until compiler, runtime, recovery, and external-effect evidence exists | There is also a procedural gap. The clean architecture worktree is one commit ahead of current Jedit `main`, and PR #300 carries these ownership corrections, From ab95368b05348cf5ea277f8e1575b15a84230a00 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:11:26 -0700 Subject: [PATCH 2/4] Freeze the matching historical closure for basis comparison probes --- CHANGELOG.md | 5 ++ .../basis-identity/Dockerfile | 2 +- .../basis-identity/README.md | 10 ++- .../application-provenance.json | 19 +++++ .../application/edict.application.json | 20 ++++++ .../vendor/jedit-text/adapter.cbor | Bin 0 -> 446 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 -> 2362 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 + .../basis-identity/build.sh | 1 + 17 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 edict/replace-range-probes/basis-identity/application-provenance.json create mode 100644 edict/replace-range-probes/basis-identity/application/edict.application.json create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.cbor create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.sha256 create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.cbor create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.sha256 create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/edict.lawpack-output.json create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.cbor create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.sha256 create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.cbor create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.sha256 create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.cbor create mode 100644 edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc95389..2502925f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Corrected the canonical ownership document to describe the executable pure + flat-text reference separately from the unfinished stateful rope and installed + release. The historical basis-identity probe now supplies its own matching + frozen lawpack closure instead of copying the current application's closure. + - Added DL-0159, a draft representation for the Edict-authored rope: schema 2 with typed Echo edges for every structural relation, canonical CBOR facts, content-addressed immutable nodes, and an import anchor tying every head to diff --git a/edict/replace-range-probes/basis-identity/Dockerfile b/edict/replace-range-probes/basis-identity/Dockerfile index 89aa2b5b..31f16b20 100644 --- a/edict/replace-range-probes/basis-identity/Dockerfile +++ b/edict/replace-range-probes/basis-identity/Dockerfile @@ -10,7 +10,7 @@ WORKDIR /echo RUN git init && git remote add origin https://github.com/flyingrobots/echo.git \ && git fetch --depth=1 origin 49e9efb68001dfd78563d18bac9359a87671e431 \ && git archive FETCH_HEAD schemas/edict-provider/package/v1 | tar -x -COPY edict/replace-range /application-source +COPY edict/replace-range-probes/basis-identity/application /application-source COPY edict/replace-range-probes/basis-identity/ReplaceRange.edict /comparison-sources/ReplaceRange.edict COPY edict/replace-range-probes/basis-identity/PayloadEquality.edict /comparison-sources/PayloadEquality.edict COPY edict/replace-range-probes/basis-identity/build.sh /comparison-build.sh diff --git a/edict/replace-range-probes/basis-identity/README.md b/edict/replace-range-probes/basis-identity/README.md index 84dab882..9b33ed21 100644 --- a/edict/replace-range-probes/basis-identity/README.md +++ b/edict/replace-range-probes/basis-identity/README.md @@ -27,8 +27,14 @@ The COPY-based image builds the existing pinned compiler `3f81f759e921a69b04fe8cf8e62e62f8f3dc7b7e` using Rust 1.94.0, fetches the existing pinned provider `49e9efb68001dfd78563d18bac9359a87671e431`, and invokes Edict's public JSONL application-build boundary. Each disposable application -copy retains the frozen application manifest and complete published lawpack; -only its source file changes. No host repository or Git directory is mounted. +copy uses `application/`, the frozen application manifest and complete published +lawpack from Jedit commit `9e6466150e43afc83966f9fe67c6ddce2f58d5bc`. +`application-provenance.json` records every retained file's SHA-256. This closure's +manifest is `sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e`, +matching both comparison sources. It is independent of the current flat-text +application, whose published closure may evolve. The build inserts only the +selected comparison source into the disposable copy. No host repository or Git +directory is mounted. The success marker is `JIM_BYTE_COMPARISON_PACKAGES_BUILT`. Output contains `head/` and `payload/`, each with the emitted executable package and its separate diff --git a/edict/replace-range-probes/basis-identity/application-provenance.json b/edict/replace-range-probes/basis-identity/application-provenance.json new file mode 100644 index 00000000..76aac684 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application-provenance.json @@ -0,0 +1,19 @@ +{ + "schema": "jedit.probe-frozen-application/v1", + "sourceCommit": "9e6466150e43afc83966f9fe67c6ddce2f58d5bc", + "sourceRoot": "edict/replace-range", + "files": { + "edict.application.json": "sha256:3182577627cd1f88bf590f3a8481ef53476c83d783aa142182e7c65eb1f72f8b", + "vendor/jedit-text/adapter.cbor": "sha256:ab1437164084cbaf453f869c4302df9e648186fe6833bcb81c1e267c8dfeb6f9", + "vendor/jedit-text/adapter.sha256": "sha256:f8f24ec4077411189f7cdc73052c2149b24036a31535e95d4254fb6393e66421", + "vendor/jedit-text/echo-operation-configuration.cbor": "sha256:ea787368881f448021f2d71c72cbadbc487e59a09e9638acfc25ec3579a3609d", + "vendor/jedit-text/echo-operation-configuration.sha256": "sha256:583070043579837cfffb4949867bb672f71f935a45e20e6ecbf7caf7483d600c", + "vendor/jedit-text/edict.lawpack-output.json": "sha256:141c0000932ba19cff687e4173e40b0a38410f70bd511c9bc51f3fb3f73ee62b", + "vendor/jedit-text/exports.cbor": "sha256:cab9c575da49cb33289e45fc45afb9d7f05558dada734be7eb3acfbbabb1d4db", + "vendor/jedit-text/exports.sha256": "sha256:9bb11d9f8153c18af53b484526d9980b05c27eb20aa97e7623d1bf1ed5a69aea", + "vendor/jedit-text/manifest.cbor": "sha256:0323ed8e9510e473d329166d421dc7d8e1da353065c1cd2dee77e6313ba9bf91", + "vendor/jedit-text/manifest.sha256": "sha256:316cf602502a45cbab89b77b6344854edee750eaf1f0c8167da48d19ead75c22", + "vendor/jedit-text/verifier-rules.cbor": "sha256:e6d445fe8b4428307c274dd1a883b848d1aa72d1194730e75928cd1f375b2464", + "vendor/jedit-text/verifier-rules.sha256": "sha256:4fd69ab96251d649e9c9528bac6188839f0210b128ffc54701b9ee4ef6e7f7b7" + } +} diff --git a/edict/replace-range-probes/basis-identity/application/edict.application.json b/edict/replace-range-probes/basis-identity/application/edict.application.json new file mode 100644 index 00000000..f3e59f82 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/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-probes/basis-identity/application/vendor/jedit-text/adapter.cbor b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.cbor new file mode 100644 index 0000000000000000000000000000000000000000..99e3ce6283224b6b1bcd4fa3d52055c2d893d555 GIT binary patch literal 446 zcmajbAx{G_6bJAwXcBOM83KX18zTV?0}@bMAX%_o`|j4Q*LLmeb|@A;1rFHY3rrD$ z!66Z7J_Z)Ly95+LHGTQNzTfKuFiD;3^Z98hiXo&O&r?8mmsfkh1;z+D?$u)pMp6c+ zRHe`;BGYb0^D_kF*2oeeD%_eLjmO3XLa2=i8HC!tl$uclUY09_kRCOn4b};*@|7@z zVJzv$P}Ywqrv|~+hV_9qFeho%vN{nG0%`S6(miL|LSAKBp-|puu_?O(obqU71wwdK zJIH8Au%jdahHh-ib(mIjp>1ezk6D{$uuJwic$J?;Z;6NoFKu Jps;|t+i${-ucrV2 literal 0 HcmV?d00001 diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.sha256 b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.sha256 new file mode 100644 index 00000000..4260e281 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/adapter.sha256 @@ -0,0 +1 @@ +sha256:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049 diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.cbor b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.cbor new file mode 100644 index 00000000..ee60987a --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.cbor @@ -0,0 +1 @@ +¢japiVersionx(echo.operation-lowering-configuration/v1kprogramKindx!compiler-produced-bounded-pure/v1 \ No newline at end of file diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.sha256 b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.sha256 new file mode 100644 index 00000000..a708943c --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/echo-operation-configuration.sha256 @@ -0,0 +1 @@ +sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4 diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/edict.lawpack-output.json b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/edict.lawpack-output.json new file mode 100644 index 00000000..59079533 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/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:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" + }, + { + "path": "manifest.sha256", + "kind": "manifestDigest", + "coordinate": "edict.lawpack/v1", + "digest": "sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e" + }, + { + "path": "exports.cbor", + "kind": "exports", + "coordinate": "jedit.text.exports/v1", + "digest": "sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a" + }, + { + "path": "exports.sha256", + "kind": "exportsDigest", + "coordinate": "jedit.text.exports/v1", + "digest": "sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a" + }, + { + "path": "echo-operation-configuration.cbor", + "kind": "localResource", + "coordinate": "echo.operation-lowering-configuration/v1", + "digest": "sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4" + }, + { + "path": "echo-operation-configuration.sha256", + "kind": "localResourceDigest", + "coordinate": "echo.operation-lowering-configuration/v1", + "digest": "sha256:bcb811b2d00d9824a256a26e48c14401c87fff509f2f75deb2ba703003a3f6d4" + }, + { + "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:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049" + }, + { + "path": "adapter.sha256", + "kind": "adapterDigest", + "coordinate": "jedit.text.echo-adapter/v1", + "digest": "sha256:b8d9f15ef18dc99691ab1e034ca40a8b8ac52a1bc1b27597284f1edbaf84f049" + } + ] +} diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.cbor b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.cbor new file mode 100644 index 0000000000000000000000000000000000000000..80925499a5d53b13815dd48e3ed972c3deffd6cb GIT binary patch literal 2362 zcmcIm&2AGh5atm$Qp%qY=ZcoL^w3t2Ktu&iqO^Sg$DU0byq>K++iq?Fi3@K*B2_#l zV=tlWRw@#umtAEuGv9nO<8OQcG%dh?n@=R7Q`!mwKLMp_ji918>uX(9aG)j%GHo?! zwAh-m%`Q|z(%!3${ibV~xVFkH1D7~gkGhyMirGypYmg*Vn#BhY>K_ADj}xc?6h=tM z!BQmBVCHIOxv2KmUvBPfz1~hTs-7f)dP7gHl%XW=fP^-UNO(j}+tF?awXwCmKLW0V zBtO#+F31`BTuyX%iqX}2C=0I%++#K}@#2NU%I6qA?y>xClVdWUS z!OP_R_eB#oB{>jw1U;v?jh3_2Dlf#;pioB~hU2Ez*GghVNn>=Hrfnm2Rijs>5* zWl|S9{`|-#@FK3J0o2Wo5zR1&E^A;g$ZCUp7<6M9pju6!;$l_0ecK zy|N=>n5eNwwG|rl{+;#(`gp%c?-1cn=H|U%x?E%!z-g&n+~0HQAgi7ynKVgv5UJoj mOmPmXRS}jRi@~4oy`j;g$QbJd-FKn+p=)W+n^E7_tNa&ZK%@r% literal 0 HcmV?d00001 diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.sha256 b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.sha256 new file mode 100644 index 00000000..87a0814b --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/exports.sha256 @@ -0,0 +1 @@ +sha256:cbbc79fd92f09f341a1fae221a2a869bb64e15c511af39333e0453cd87fe456a diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.cbor b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.cbor new file mode 100644 index 0000000000000000000000000000000000000000..7bbc2c452f94541ff049a5d8b88f272775da67f6 GIT binary patch literal 787 zcmZ3Ll$ny1m70=SqF0hyQIeioQIKC$QoIN#T#69Zg9_`H8K$LVrl%H{G^G`1BpR8T zMJSx!Q~7t&hxsN_^6QkOwAyBG^AkNPxZcv(jwSeP`#;yL^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&Pw<`7!Qe z@5yNsSIaT`EaB?zI;tgoa8v1Y4S%`Y>svl}0=<`<3bZ681Z<;c5y*yokg0mb1&MjO znMK%4We69m%lOJO^UgVrYCD;0+e>F4%N$3%Hr2LdiXddPd};fnHD=Q?D^Yha3i*gh5l2hF>D@sa>Qh_Q9O2LV=LIEk%f>H}|5|dMd67$ki w_411nlXFtBxM$~`?K}2Vzp8oE3QdM`|jog00D|fM*si- literal 0 HcmV?d00001 diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.sha256 b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.sha256 new file mode 100644 index 00000000..be4e2d8b --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/manifest.sha256 @@ -0,0 +1 @@ +sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.cbor b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.cbor new file mode 100644 index 00000000..ef8836e5 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.cbor @@ -0,0 +1 @@ +¤fschemasjedit.text.schema@1lschemaDigestxGsha256:f49b0ff6c9c7010d3a72b46734d47832853f496739a083edc41dddec3431bac7oruntimeOntologyugeneric-bounded-graphworacleIsExecutableInputô \ No newline at end of file diff --git a/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.sha256 b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.sha256 new file mode 100644 index 00000000..f8f7a387 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/application/vendor/jedit-text/verifier-rules.sha256 @@ -0,0 +1 @@ +sha256:364fb4ff98ae64fcd8d793aae3d7cb133ff8a1ce661330cea47688b13a0a823f diff --git a/edict/replace-range-probes/basis-identity/build.sh b/edict/replace-range-probes/basis-identity/build.sh index 39148e78..800f13a5 100644 --- a/edict/replace-range-probes/basis-identity/build.sh +++ b/edict/replace-range-probes/basis-identity/build.sh @@ -12,6 +12,7 @@ for comparison in head payload; do application="/comparison-$comparison" mkdir "$application" cp -R /application-source/. "$application/" + mkdir -p "$application/src" cp "/comparison-sources/$source_file" "$application/src/ReplaceRange.edict" mkdir -p "$application/.build" cp -R /echo/schemas/edict-provider/package/v1 "$application/.build/echo-provider" From eba191c14db81228cdcc8500e6f40716a1421470 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:28:05 -0700 Subject: [PATCH 3/4] Refuse historical closure drift before comparison compilation --- CHANGELOG.md | 1 + .../basis-identity/Dockerfile | 13 ++++- .../basis-identity/README.md | 5 +- .../basis-identity/build.sh | 1 + .../basis-identity/verify-closure.mjs | 58 +++++++++++++++++++ .../basis-identity/verify-closure.spec.mjs | 50 ++++++++++++++++ 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 edict/replace-range-probes/basis-identity/verify-closure.mjs create mode 100644 edict/replace-range-probes/basis-identity/verify-closure.spec.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2502925f..77e1dfc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ flat-text reference separately from the unfinished stateful rope and installed release. The historical basis-identity probe now supplies its own matching frozen lawpack closure instead of copying the current application's closure. + Build-time and runtime checksum checks refuse historical input drift. - Added DL-0159, a draft representation for the Edict-authored rope: schema 2 with typed Echo edges for every structural relation, canonical CBOR facts, diff --git a/edict/replace-range-probes/basis-identity/Dockerfile b/edict/replace-range-probes/basis-identity/Dockerfile index 31f16b20..a837b995 100644 --- a/edict/replace-range-probes/basis-identity/Dockerfile +++ b/edict/replace-range-probes/basis-identity/Dockerfile @@ -1,5 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 # © James Ross Ω FLYING•ROBOTS +FROM node:22.23.1-bookworm AS historical-closure +WORKDIR /historical +COPY edict/replace-range-probes/basis-identity/application ./application +COPY edict/replace-range-probes/basis-identity/application-provenance.json ./application-provenance.json +COPY edict/replace-range-probes/basis-identity/verify-closure.mjs ./verify-closure.mjs +COPY edict/replace-range-probes/basis-identity/verify-closure.spec.mjs ./verify-closure.spec.mjs +RUN node --test verify-closure.spec.mjs \ + && node verify-closure.mjs /historical/application /historical/application-provenance.json /application-checksums.sha256 + FROM rust:1.94.0-bookworm WORKDIR /edict RUN git init && git remote add origin https://github.com/flyingrobots/edict.git \ @@ -10,7 +19,9 @@ WORKDIR /echo RUN git init && git remote add origin https://github.com/flyingrobots/echo.git \ && git fetch --depth=1 origin 49e9efb68001dfd78563d18bac9359a87671e431 \ && git archive FETCH_HEAD schemas/edict-provider/package/v1 | tar -x -COPY edict/replace-range-probes/basis-identity/application /application-source +COPY --from=historical-closure /historical/application /application-source +COPY --from=historical-closure /historical/application-provenance.json /application-provenance.json +COPY --from=historical-closure /application-checksums.sha256 /application-checksums.sha256 COPY edict/replace-range-probes/basis-identity/ReplaceRange.edict /comparison-sources/ReplaceRange.edict COPY edict/replace-range-probes/basis-identity/PayloadEquality.edict /comparison-sources/PayloadEquality.edict COPY edict/replace-range-probes/basis-identity/build.sh /comparison-build.sh diff --git a/edict/replace-range-probes/basis-identity/README.md b/edict/replace-range-probes/basis-identity/README.md index 9b33ed21..80f80214 100644 --- a/edict/replace-range-probes/basis-identity/README.md +++ b/edict/replace-range-probes/basis-identity/README.md @@ -29,7 +29,10 @@ existing pinned provider `49e9efb68001dfd78563d18bac9359a87671e431`, and invokes Edict's public JSONL application-build boundary. Each disposable application copy uses `application/`, the frozen application manifest and complete published lawpack from Jedit commit `9e6466150e43afc83966f9fe67c6ddce2f58d5bc`. -`application-provenance.json` records every retained file's SHA-256. This closure's +`application-provenance.json` records every retained file's SHA-256. The Node +preflight stage checks every hash, refuses missing or unrecorded files and +unsafe paths, then emits a checksum projection that also binds the provenance +JSON. The runtime build checks that projection before compiling either source. This closure's manifest is `sha256:95758c1605894672cc9069fde01bb8b6e11842b053102660c9cd4f4d6f34d64e`, matching both comparison sources. It is independent of the current flat-text application, whose published closure may evolve. The build inserts only the diff --git a/edict/replace-range-probes/basis-identity/build.sh b/edict/replace-range-probes/basis-identity/build.sh index 800f13a5..cda772d5 100644 --- a/edict/replace-range-probes/basis-identity/build.sh +++ b/edict/replace-range-probes/basis-identity/build.sh @@ -3,6 +3,7 @@ # © James Ross Ω FLYING•ROBOTS set -eu [ -f /.dockerenv ] || { echo 'Run in the COPY-based Docker image' >&2; exit 1; } +(cd /application-source && sha256sum --check /application-checksums.sha256) mkdir /comparison-output for comparison in head payload; do case "$comparison" in diff --git a/edict/replace-range-probes/basis-identity/verify-closure.mjs b/edict/replace-range-probes/basis-identity/verify-closure.mjs new file mode 100644 index 00000000..3663f216 --- /dev/null +++ b/edict/replace-range-probes/basis-identity/verify-closure.mjs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +import { createHash } from 'node:crypto'; +import { lstatSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; + +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); + +try { + const [application, provenance, output] = process.argv.slice(2); + if (!application || !provenance || !output) throw new Error('expected application, provenance and checksum output'); + const root = resolve(application); + const receiptBytes = readFileSync(provenance); + const receipt = JSON.parse(receiptBytes); + if (receipt.schema !== 'jedit.probe-frozen-application/v1' || + !/^[a-f0-9]{40}$/.test(receipt.sourceCommit) || + receipt.files === null || typeof receipt.files !== 'object' || Array.isArray(receipt.files)) { + throw new Error('invalid provenance envelope'); + } + const recorded = Object.entries(receipt.files); + if (recorded.length === 0) throw new Error('empty historical closure'); + const actual = []; + function walk(directory, prefix = '') { + for (const name of readdirSync(directory).sort()) { + const path = join(directory, name); + const entry = lstatSync(path); + const key = prefix + name; + if (entry.isDirectory()) walk(path, `${key}/`); + else if (entry.isFile()) actual.push(key); + else throw new Error(`nonregular historical input: ${key}`); + } + } + walk(root); + const checksums = []; + for (const [path, expected] of recorded) { + if (path.startsWith('/') || /[\\\r\n\t\0]/.test(path) || + path.split('/').some(part => part === '' || part === '.' || part === '..') || + typeof expected !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(expected)) { + throw new Error('invalid recorded path or digest'); + } + const bytes = readFileSync(join(root, path)); + if (`sha256:${hash(bytes)}` !== expected) throw new Error(`historical hash mismatch: ${path}`); + checksums.push(`${expected.slice(7)} ${path}`); + } + const listed = new Set(recorded.map(([path]) => path)); + if (actual.length !== listed.size || actual.some(path => !listed.has(path))) { + throw new Error('unrecorded historical input'); + } + // Bind the JSON envelope too, so runtime verification detects receipt drift. + const receiptPath = relative(root, resolve(provenance)); + if (/[\\\r\n\t\0]/.test(receiptPath)) throw new Error('invalid provenance path'); + checksums.push(`${hash(receiptBytes)} ${receiptPath}`); + writeFileSync(output, `${checksums.join('\n')}\n`); + process.stdout.write(`${JSON.stringify({ kind: 'FrozenClosureVerified', files: recorded.length })}\n`); +} catch (error) { + process.stderr.write(`${JSON.stringify({ kind: 'InvalidFrozenClosure', detail: error instanceof Error ? error.message : 'invalid closure' })}\n`); + process.exitCode = 1; +} diff --git a/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs b/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs new file mode 100644 index 00000000..427d2a6b --- /dev/null +++ b/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { cpSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +const checker = join(root, 'verify-closure.mjs'); + +function runCase(change) { + const scratch = mkdtempSync(join(tmpdir(), 'jedit-basis-closure-')); + try { + const application = join(scratch, 'application'); + const provenance = join(scratch, 'application-provenance.json'); + cpSync(join(root, 'application'), application, { recursive: true }); + cpSync(join(root, 'application-provenance.json'), provenance); + change?.(application, provenance); + const result = spawnSync(process.execPath, [checker, application, provenance, join(scratch, 'checksums')], { encoding: 'utf8' }); + return { status: result.status, report: JSON.parse(result.status === 0 ? result.stdout : result.stderr) }; + } finally { + rmSync(scratch, { recursive: true }); + } +} + +test('the retained closure passes before producing the checksum projection', () => { + const result = runCase(); + assert.equal(result.status, 0); + assert.equal(result.report.files, 12); +}); + +for (const [name, mutate] of [ + ['tampered file', app => writeFileSync(join(app, 'vendor/jedit-text/manifest.cbor'), Buffer.from([0]))], + ['missing file', app => unlinkSync(join(app, 'vendor/jedit-text/manifest.cbor'))], + ['unrecorded file', app => writeFileSync(join(app, 'unrecorded'), 'extra')], + ['escaping receipt path', (app, provenance) => { + const value = JSON.parse(readFileSync(provenance, 'utf8')); + value.files['../outside'] = `sha256:${'0'.repeat(64)}`; + writeFileSync(provenance, JSON.stringify(value)); + }], +]) { + test(`${name} refuses before compilation`, () => { + const result = runCase(mutate); + assert.equal(result.status, 1); + assert.equal(result.report.kind, 'InvalidFrozenClosure'); + }); +} From 85e7b0aeda5fe69bca1164066e7d2eb99bbae35f Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 23:35:05 -0700 Subject: [PATCH 4/4] Guard frozen closure reads against leaf symlink swaps --- .../basis-identity/README.md | 7 ++ .../basis-identity/verify-closure.mjs | 20 +++++- .../basis-identity/verify-closure.spec.mjs | 68 +++++++++++++++++-- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/edict/replace-range-probes/basis-identity/README.md b/edict/replace-range-probes/basis-identity/README.md index 80f80214..3dbc1637 100644 --- a/edict/replace-range-probes/basis-identity/README.md +++ b/edict/replace-range-probes/basis-identity/README.md @@ -51,3 +51,10 @@ metered byte equality; it does not provide snapshot reads or make the full ReplaceRange acceptance criteria complete. Generic fact reads, application-owned codecs and rope semantics, admitted execution, independent oracle agreement, Tick, receipts, WAL, and recovery remain required by #296. + +The closure checker requires a build-owned immutable input tree, as supplied by +Docker `COPY`. It rechecks directories before descent and reads regular files +through descriptors opened with `O_NOFOLLOW`, including the provenance JSON. +This refuses a leaf symlink swapped after traversal. It does not provide a +sandbox for a hostile process concurrently replacing ancestor directories; +mutable trees shared with untrusted writers are outside this probe's contract. diff --git a/edict/replace-range-probes/basis-identity/verify-closure.mjs b/edict/replace-range-probes/basis-identity/verify-closure.mjs index 3663f216..be723dca 100644 --- a/edict/replace-range-probes/basis-identity/verify-closure.mjs +++ b/edict/replace-range-probes/basis-identity/verify-closure.mjs @@ -1,16 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS import { createHash } from 'node:crypto'; -import { lstatSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { join, relative, resolve } from 'node:path'; const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +// The build owns an immutable COPY snapshot. Leaf descriptors also refuse links +// swapped after traversal; this is not a sandbox for hostile mutable ancestors. +function readRegular(path) { + if (typeof constants.O_NOFOLLOW !== 'number') throw new Error('O_NOFOLLOW is required'); + const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + if (!fstatSync(fd).isFile()) throw new Error('nonregular historical input'); + return readFileSync(fd); + } finally { + closeSync(fd); + } +} + try { const [application, provenance, output] = process.argv.slice(2); if (!application || !provenance || !output) throw new Error('expected application, provenance and checksum output'); const root = resolve(application); - const receiptBytes = readFileSync(provenance); + const receiptBytes = readRegular(provenance); const receipt = JSON.parse(receiptBytes); if (receipt.schema !== 'jedit.probe-frozen-application/v1' || !/^[a-f0-9]{40}$/.test(receipt.sourceCommit) || @@ -21,6 +34,7 @@ try { if (recorded.length === 0) throw new Error('empty historical closure'); const actual = []; function walk(directory, prefix = '') { + if (!lstatSync(directory).isDirectory()) throw new Error('nonregular historical directory'); for (const name of readdirSync(directory).sort()) { const path = join(directory, name); const entry = lstatSync(path); @@ -38,7 +52,7 @@ try { typeof expected !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(expected)) { throw new Error('invalid recorded path or digest'); } - const bytes = readFileSync(join(root, path)); + const bytes = readRegular(join(root, path)); if (`sha256:${hash(bytes)}` !== expected) throw new Error(`historical hash mismatch: ${path}`); checksums.push(`${expected.slice(7)} ${path}`); } diff --git a/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs b/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs index 427d2a6b..1eeaf362 100644 --- a/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs +++ b/edict/replace-range-probes/basis-identity/verify-closure.spec.mjs @@ -2,16 +2,16 @@ // © James Ross Ω FLYING•ROBOTS import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { cpSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'; +import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import test from 'node:test'; const root = fileURLToPath(new URL('.', import.meta.url)); const checker = join(root, 'verify-closure.mjs'); -function runCase(change) { +function runCase(change, swapBeforeRead = false) { const scratch = mkdtempSync(join(tmpdir(), 'jedit-basis-closure-')); try { const application = join(scratch, 'application'); @@ -19,8 +19,39 @@ function runCase(change) { cpSync(join(root, 'application'), application, { recursive: true }); cpSync(join(root, 'application-provenance.json'), provenance); change?.(application, provenance); - const result = spawnSync(process.execPath, [checker, application, provenance, join(scratch, 'checksums')], { encoding: 'utf8' }); - return { status: result.status, report: JSON.parse(result.status === 0 ? result.stdout : result.stderr) }; + const args = []; + const marker = join(scratch, 'swapped'); + if (swapBeforeRead) { + const victim = join(application, 'vendor/jedit-text/manifest.cbor'); + const target = join(scratch, 'matching-bytes'); + writeFileSync(target, readFileSync(victim)); + const loader = join(scratch, 'swap.mjs'); + // Synchronize a real filesystem swap immediately before the leaf read. + writeFileSync(loader, ` + import fs from 'node:fs'; + import { syncBuiltinESMExports } from 'node:module'; + let swapped = false; + const swap = path => { + if (!swapped && path === ${JSON.stringify(victim)}) { + swapped = true; + fs.unlinkSync(path); + fs.symlinkSync(${JSON.stringify(target)}, path); + fs.writeFileSync(${JSON.stringify(marker)}, 'swapped'); + } + }; + for (const method of ['openSync', 'readFileSync']) { + const original = fs[method]; + fs[method] = function(path, ...args) { + swap(path); + return original.call(fs, path, ...args); + }; + } + syncBuiltinESMExports(); + `); + args.push('--import', pathToFileURL(loader).href); + } + const result = spawnSync(process.execPath, [...args, checker, application, provenance, join(scratch, 'checksums')], { encoding: 'utf8' }); + return { swapped: existsSync(marker), status: result.status, report: JSON.parse(result.status === 0 ? result.stdout : result.stderr) }; } finally { rmSync(scratch, { recursive: true }); } @@ -36,6 +67,24 @@ for (const [name, mutate] of [ ['tampered file', app => writeFileSync(join(app, 'vendor/jedit-text/manifest.cbor'), Buffer.from([0]))], ['missing file', app => unlinkSync(join(app, 'vendor/jedit-text/manifest.cbor'))], ['unrecorded file', app => writeFileSync(join(app, 'unrecorded'), 'extra')], + ['matching-byte symlink', app => { + const victim = join(app, 'vendor/jedit-text/manifest.cbor'); + const target = join(app, '..', 'matching-bytes'); + writeFileSync(target, readFileSync(victim)); + unlinkSync(victim); + symlinkSync(target, victim); + }], + ['provenance symlink', (app, provenance) => { + const target = `${provenance}.original`; + writeFileSync(target, readFileSync(provenance)); + unlinkSync(provenance); + symlinkSync(target, provenance); + }], + ...['schema', 'sourceCommit'].map(field => [`invalid ${field}`, (app, provenance) => { + const value = JSON.parse(readFileSync(provenance, 'utf8')); + value[field] = 'invalid'; + writeFileSync(provenance, JSON.stringify(value)); + }]), ['escaping receipt path', (app, provenance) => { const value = JSON.parse(readFileSync(provenance, 'utf8')); value.files['../outside'] = `sha256:${'0'.repeat(64)}`; @@ -46,5 +95,14 @@ for (const [name, mutate] of [ const result = runCase(mutate); assert.equal(result.status, 1); assert.equal(result.report.kind, 'InvalidFrozenClosure'); + if (name === 'escaping receipt path') assert.equal(result.report.detail, 'invalid recorded path or digest'); + if (name.startsWith('invalid ')) assert.equal(result.report.detail, 'invalid provenance envelope'); }); } + +test('a matching-byte symlink swapped after traversal refuses at the read boundary', () => { + const result = runCase(undefined, true); + assert.equal(result.swapped, true); + assert.equal(result.status, 1); + assert.equal(result.report.kind, 'InvalidFrozenClosure'); +});