Skip to content

Commit 356c035

Browse files
committed
feat(identity #a): references carry DURABLE identity (bound_to) — O(1) rename, red->green
References now point at the binding's stable @mod#int via a durable bound_to edge (persisted to the flat log), not by spelling. The resolver prefers it; a rename changes only the binding's display name, references stay untouched, and a COLD re-render follows identity to the current name instead of re-deriving by spelling and missing the renamed def. - resolve.clj: BOUND dynamic + 3 bind sites; bound-target; refers-target prefers bound_to; walk symbol arm resolves edged leaves by identity. - daemon: read-hidden-preds (bound_to filtered from :query/datalog/warm-cache, option-1) but DURABLE on the log + survives strip (not a resolve-pred); persist-bound-for-rename!; the :identity-deferred reject REMOVED. Scope held: existing sequential @mod#int ids, NO content-hashes (increment (b), deferred). RECEIPTS: - R4 acceptance (cnf_edit_min_rename.clj, VERIFIED IN MAIN): rename ok 2 ops, new name in cold render, old gone, recompiles -> PASS. The pre-existing red receipt, unmodified, now green. - R1 durable edge: 2 bound_to lines in the flat log (@Schema#284,#342 -> #174). - R5 no CRDT regression: cnf_crdt_insert_receipt + cnf_render_crdt_receipt PASS. - R6 read-side: :warm-check consistent; :query surfaces 0 bound_to / 0 refers_to rows. Dangling-edge (concurrent delete-of-target) noted as the gate-v2 follow-on, deferred.
1 parent af89246 commit 356c035

4 files changed

Lines changed: 187 additions & 18 deletions

File tree

chartroom/src/resolve.clj

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
;; render-mode marker predicate value-ids — DYNAMIC, rebound (recomputed against
7979
;; the fresh store) inside `resolve-edn!`; store-local ids must match their store.
8080
(def ^:dynamic Vp nil) (def ^:dynamic KIND nil) (def ^:dynamic REFERS nil)
81+
(def ^:dynamic BOUND nil) ; #(a) the DURABLE identity edge `bound_to` (persisted; reference -> binding's @mod#int)
8182
(def ^:dynamic FIXED nil) (def ^:dynamic QUAL nil) ; render-mode markers
8283
(def ^:dynamic CTOR nil) ; a `->Name` auto-constructor ref: render `->` + the type's name
8384
(def ^:dynamic ACC nil) ; a synth field accessor `<lower(Name)>-<field>`: stores the field
@@ -144,10 +145,17 @@
144145
(when (and (string? p) (re-matches #"seg\d+" p)) [(parse-long (subs p 3)) (:r cl)]))))
145146
(sort-by first) (mapv second)))
146147
(defn head-sym [e] (when (= "list" (kind-of e)) (sym-val (first (ordered-children e)))))
148+
(defn bound-target
149+
"The DURABLE identity target of reference `L` — the bound_to edge points at the binding's
150+
stable @mod#int node-id, not its spelling. nil if L carries no durable edge (legacy/unedged
151+
refs fall back to the spelling-derived refers_to)."
152+
[L] (when BOUND (let [cs (c/by-lp ctx L BOUND)] (when-let [cid (select-main-1 cs)] (:r (c/claim-of ctx cid))))))
147153
(defn refers-target
148-
"The binding node that reference `L` resolves to in the default-main view (SELECTS via
149-
select-main-1 over the derived refers_to group; re-derived per resolve). Not a uniqueness proof."
150-
[L] (let [cs (c/by-lp ctx L REFERS)] (when-let [cid (select-main-1 cs)] (:r (c/claim-of ctx cid)))))
154+
"The binding node reference `L` resolves to (default-main view). Prefers the DURABLE bound_to
155+
identity edge; falls back to the derived refers_to (spelling-walk) for unedged/legacy refs.
156+
Not a uniqueness proof (select-main-1)."
157+
[L] (or (bound-target L)
158+
(let [cs (c/by-lp ctx L REFERS)] (when-let [cid (select-main-1 cs)] (:r (c/claim-of ctx cid))))))
151159
(defn live-node? [e] (seq (c/by-lp ctx e KIND)))
152160

153161
;; --- binding extraction -----------------------------------------------------
@@ -308,6 +316,10 @@
308316
(let [nm (sym-val node)
309317
local (some #(get % nm) scope)] ; nearest frame binding nm
310318
(cond
319+
;; #(a) identity: a reference with a DURABLE bound_to edge resolves by IDENTITY
320+
;; (binding's stable @mod#int), not by spelling — so a target rename (display-name only)
321+
;; leaves this intact and render follows it to the target's CURRENT name. Spelling is the fallback.
322+
(bound-target node) (bind! node (bound-target node))
311323
local (bind! node local)
312324
;; free symbol: cross-module value/type import (:refer/:rename/:as), else a
313325
;; module-local TYPE used in value position (a constructor `(Point ...)` /
@@ -647,7 +659,7 @@
647659
(c/set-supersedes-pred! store sup)
648660
(binding [ctx store, tx t, SUP sup
649661
file->ents (atom {})
650-
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to")
662+
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to") BOUND (c/value! store "bound_to")
651663
FIXED (c/value! store "keep_spelling") QUAL (c/value! store "qualifier")
652664
CTOR (c/value! store "ctor_prefix") ACC (c/value! store "accessor_field")
653665
n-resolved (atom 0) n-unresolved (atom 0) n-xmod (atom 0) n-type (atom 0) n-comment (atom 0)
@@ -776,7 +788,7 @@
776788
(c/set-supersedes-pred! store sup)
777789
(binding [ctx store, tx t, SUP sup
778790
file->ents (atom {})
779-
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to")
791+
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to") BOUND (c/value! store "bound_to")
780792
FIXED (c/value! store "keep_spelling") QUAL (c/value! store "qualifier")
781793
CTOR (c/value! store "ctor_prefix") ACC (c/value! store "accessor_field")
782794
n-resolved (atom 0) n-unresolved (atom 0) n-xmod (atom 0) n-type (atom 0) n-comment (atom 0)
@@ -809,7 +821,7 @@
809821
(c/set-supersedes-pred! store sup)
810822
(binding [ctx store, tx t, SUP sup
811823
file->ents (atom {})
812-
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to")
824+
Vp (c/value! store "v") KIND (c/value! store "kind") REFERS (c/value! store "refers_to") BOUND (c/value! store "bound_to")
813825
FIXED (c/value! store "keep_spelling") QUAL (c/value! store "qualifier")
814826
CTOR (c/value! store "ctor_prefix") ACC (c/value! store "accessor_field")
815827
n-resolved (atom 0) n-unresolved (atom 0) n-xmod (atom 0) n-type (atom 0) n-comment (atom 0)

cnf_bound_identity_receipt.clj

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
;; ============================================================================
2+
;; cnf_bound_identity_receipt.clj — #(a) identity arc: bound_to is DURABLE but
3+
;; FILTERED from read projections. R1 (durable edge) + R6 (read-side unaffected).
4+
;; bb -cp out cnf_bound_identity_receipt.clj
5+
;;
6+
;; After an :edit-min rename on an in-process /tmp daemon:
7+
;; R1 the flat log carries durable `bound_to` lines (count > 0).
8+
;; R6a :warm-check returns :consistent true (the incrementally-maintained warm
9+
;; cache == a fresh whole rebuild — persisting+filtering bound_to didn't
10+
;; diverge the cache).
11+
;; R6b a :query for ALL triples does NOT surface any `bound_to` triple
12+
;; (read-hidden-preds keeps the durable identity edge out of :query/datalog/
13+
;; warm-cache — option-1 scope: render+resolve read it off the store directly).
14+
;;
15+
;; SAFE: a /tmp COPY of .fram/code.log + in-process daemon (boot-flat!, handle{});
16+
;; NO socket, NEVER port 7977, NEVER the canonical lodestar log.
17+
;; ============================================================================
18+
(require '[clojure.java.io :as io] '[clojure.string :as str]
19+
'[fram.cnf :as c] '[fram.fold :as fold] '[fram.query :as q] '[fram.rt])
20+
(def home (System/getProperty "user.home"))
21+
(def root (System/getProperty "user.dir"))
22+
(def code-log (str root "/.fram/code.log"))
23+
(when-not (.exists (io/file code-log)) (println "SKIP — missing" code-log) (System/exit 0))
24+
(binding [*command-line-args* []] (load-file "cnf_coord_daemon.clj"))
25+
26+
;; /tmp copy — the daemon writes bound_to back into THIS file, never the source.
27+
(def flat (str (System/getProperty "java.io.tmpdir") "/bound-identity-" (System/nanoTime) ".code.log"))
28+
(io/copy (io/file code-log) (io/file flat))
29+
(boot-flat! flat)
30+
31+
;; rename a real schema helper with in-module references (same target the R4 receipt uses).
32+
(def resp (handle {:op :edit-min :spec {:op "rename" :module "schema" :old "replace!" :new "supersede-prior!"}}))
33+
(println "rename replace! -> supersede-prior!:" (pr-str resp))
34+
35+
;; ---- R1: durable bound_to lines in the flat log ----------------------------
36+
(def flat-txt (slurp flat))
37+
(def bound-lines (->> (str/split-lines flat-txt) (filter #(str/includes? % "\"bound_to\"")) vec))
38+
(def r1 (pos? (count bound-lines)))
39+
(println "\n=== R1 — durable bound_to in the flat log ===")
40+
(println " bound_to lines:" (count bound-lines))
41+
(doseq [l (take 4 bound-lines)] (println " " l))
42+
43+
;; ---- R6a: warm cache stays consistent with a fresh rebuild -----------------
44+
(def wc (handle {:op :warm-check}))
45+
(def r6a (true? (:consistent wc)))
46+
(println "\n=== R6a — warm-check after the rename ===")
47+
(println " :consistent" (:consistent wc) " inc-triples" (:inc-triples wc) " fresh-triples" (:fresh-triples wc))
48+
49+
;; ---- R6b: a :query for ALL triples surfaces NO bound_to --------------------
50+
;; Datalog over the base `triple(l,p,r)` relation, binding p so we read the predicate.
51+
;; If bound_to leaked into the read projection it would appear as a (l "bound_to" r) row.
52+
(def ALLQ {:find "out"
53+
:rules [{:head {:rel "out" :args [{:var "l"} {:var "p"} {:var "r"}]}
54+
:body [{:rel "triple" :args [{:var "l"} {:var "p"} {:var "r"}]}]}]})
55+
(def qres (handle {:op :query :query ALLQ}))
56+
(def all-rows (:ok qres))
57+
(def bound-rows (filter (fn [[_ p _]] (= "bound_to" p)) all-rows))
58+
;; also confirm refers_to (a resolve-pred) is absent (sanity: read-side really is filtered)
59+
(def refers-rows (filter (fn [[_ p _]] (= "refers_to" p)) all-rows))
60+
(def r6b (and (seq all-rows) (empty? bound-rows) (empty? refers-rows)))
61+
(println "\n=== R6b — :query (all triples) does NOT surface bound_to ===")
62+
(println " total triple rows:" (count all-rows)
63+
" bound_to rows:" (count bound-rows)
64+
" refers_to rows:" (count refers-rows)
65+
" engine:" (:engine qres))
66+
67+
(println "\n=== VERDICT ===")
68+
(if (and (:ok resp) r1 r6a r6b)
69+
(do (println "PASS — bound_to is DURABLE (R1: flat-log lines) yet INVISIBLE to the read view"
70+
"(R6a: warm cache consistent; R6b: :query surfaces zero bound_to/refers_to rows).")
71+
(System/exit 0))
72+
(do (println "FAIL —"
73+
"rename-ok" (boolean (:ok resp)) "R1" r1 "R6a" r6a "R6b" r6b)
74+
(System/exit 1)))

cnf_coord_daemon.clj

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
;; (so :query/:warm-check/the read view never see them); the materialize step rolls
4949
;; back the seq-space the resolver's tx consumed.
5050
(def resolve-preds #{"refers_to" "keep_spelling" "qualifier" "ctor_prefix" "accessor_field" "supersedes"})
51+
;; #(a) identity: bound_to is DURABLE (persisted to the flat log — an authored identity edge,
52+
;; reference -> binding's stable @mod#int) but for option-1 scope is kept OUT of read projections
53+
;; (:query/datalog/warm-cache/tripwire) — render+resolve read it off the store directly. Filtered
54+
;; HERE (read view) WITHOUT being a resolve-pred (which would strip it from the store + roll back seq).
55+
(def read-hidden-preds #{"bound_to"})
5156
(def refers-version (atom -1)) ; the co version refers_to was last materialized at
5257

5358
;; ---- S3.3: scoped re-resolve state -----------------------------------------
@@ -171,7 +176,7 @@
171176
;; claims the flat log ingested, identical whether or not refers_to has been materialized.
172177
(defn- claim->triple [st cid]
173178
(let [cl (c/claim-of st cid) pstr (c/literal st (:p cl))]
174-
(when-not (or (schema-preds pstr) (resolve-preds pstr))
179+
(when-not (or (schema-preds pstr) (resolve-preds pstr) (read-hidden-preds pstr))
175180
[(s/name-of st (:l cl)) pstr
176181
(if (c/value-object? st (:r cl)) (c/literal st (:r cl)) (s/name-of st (:r cl)))])))
177182

@@ -830,6 +835,34 @@
830835
op))
831836
asserts))
832837

838+
;; #(a) O(1) rename precondition: before renaming a def, PERSIST a durable bound_to edge from
839+
;; every reference of it to the def's stable @mod#int. The rename then changes only the binding's
840+
;; display name; references keep their identity edge and a cold re-render follows it to the CURRENT
841+
;; name (instead of re-deriving by spelling and missing the renamed def). Idempotent.
842+
;;
843+
;; Runs under dlock (caller). ensure-refers! first materializes the warm refers_to (spelling-derived
844+
;; on the first rename; identity-preserving on later ones) over `co`; B = the def binding node-id
845+
;; (def-binding via with-resolve-read, the SAME node refers_to points at). For each reference leaf
846+
;; whose refers_to lands on B, do-assert a `bound_to` link (leaf -> B's @mod#int). do-assert appends
847+
;; to the flat log (durable) and commits to the warm store; bound_to is multi-valued + survives
848+
;; strip-resolve-claims! (not a resolve-pred) and is filtered from read projections (read-hidden-preds).
849+
(defn- persist-bound-for-rename! [spec]
850+
(ensure-refers!) ; materialize warm refers_to (frames + edges)
851+
(let [st (:store @co)
852+
REFp (c/value-id st "refers_to")
853+
B (target-node {:module (:module spec) :name (:old spec)})]
854+
(when (and B REFp)
855+
(let [BND (or (c/value-id st "bound_to") (c/value! st "bound_to"))
856+
v0 (current-seq @co)
857+
B-name (s/name-of st B)
858+
already (set (map #(:l (c/claim-of st %)) (c/by-p st BND)))
859+
ref-leaves (->> (c/by-p st REFp)
860+
(map #(c/claim-of st %))
861+
(filter #(= B (:r %)))
862+
(map :l) distinct)]
863+
(doseq [leaf ref-leaves :when (not (already leaf))]
864+
(do-assert (s/name-of st leaf) "bound_to" B-name v0))))))
865+
833866
;; do-edit-min: run the verb over a CLONE, harvest its minimal delta as wire ops, and
834867
;; commit those through do-assert/do-retract on the REAL `co`. te-naming for new nodes
835868
;; is assigned here (the verb mints nameless local entities on the clone).
@@ -842,18 +875,15 @@
842875
(when-not (#{"set-body" "upsert-form" "insert-form" "rename"} (:op spec))
843876
(throw (ex-info (str "edit-min: unknown verb '" (:op spec) "' (known: set-body, upsert-form, rename)")
844877
{:reject :unknown-verb})))
845-
;; GRAPH RENAME IS IDENTITY-DEFERRED — reject, do NOT silently do the wrong thing.
846-
;; verb-rename! is O(1): it rewrites the DEF binding's spelling only and relies on
847-
;; references following refers_to (identity). On the mainline SPELLING+derive model a
848-
;; cold render re-derives refers_to BY SPELLING, so old-spelled references can't re-resolve
849-
;; to the renamed def → the rename renders the OLD name (measured: cnf_rename_spelling_check.clj
850-
;; — 2 ops, def rewritten, 2 reference leaves still "replace!"). A correct mainline rename is
851-
;; O(N) (rewrite every reference spelling) and is NOT built. Same identity-deferral as gate-v2
852-
;; (docs/VIEWS_AND_BRANCHES.md). Re-enable when references carry identity OR an O(N) rewrite
853-
;; verb lands. (The text-path CLI rename still works as a same-process projection.)
878+
;; #(a) GRAPH RENAME IS NOW O(1) — references carry DURABLE identity. verb-rename! rewrites
879+
;; the DEF binding's spelling only; references follow `bound_to` (the binding's stable @mod#int),
880+
;; persisted HERE before the rename so a cold re-render resolves them by IDENTITY, not by spelling
881+
;; (the old failure: cnf_rename_spelling_check.clj — old-spelled refs re-derived to nothing and
882+
;; rendered the OLD name). persist-bound-for-rename! is idempotent + appends bound_to to the flat
883+
;; log (durable). Content-hashes (increment (b)) are explicitly OUT of scope: identity is the
884+
;; existing sequential @mod#int. (The text-path CLI rename still works as a same-process projection.)
854885
(when (= "rename" (:op spec))
855-
(throw (ex-info "rename requires identity refs; deferred — O(1) graph rename needs identity, mainline is spelling+derive (O(N) spelling-rewrite not built). See docs/VIEWS_AND_BRANCHES.md."
856-
{:reject :identity-deferred})))
886+
(locking dlock (persist-bound-for-rename! spec)))
857887
(let [real (:store @co)
858888
clone (atom @real) ; O(1) structural clone; verb writes here only
859889
since (:next-id @clone)

cnf_edit_min_rename.clj

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
;; Build B — confirm the RENAME verb (the one whose no-capture check READS refers_to)
2+
;; still works on the minimal-op path with the whole-corpus walk OFF: do-edit-min
3+
;; pre-materializes refers_to (ensure-refers!), the clone inherits it, the verb's
4+
;; capture-check reads the inherited edges. Rename a schema def + confirm the new name
5+
;; lands in render(log) and it recompiles. NEVER 7977.
6+
(require '[fram.cnf :as c] '[fram.schema :as s]
7+
'[clojure.string :as str] '[clojure.edn :as edn] '[clojure.java.io :as io] '[babashka.process :as proc])
8+
(def home (System/getProperty "user.home"))
9+
(def root (System/getProperty "user.dir"))
10+
(def beagle-home (or (System/getenv "BEAGLE_HOME") (str home "/code/beagle")))
11+
(def roundtrip-rkt (or (System/getenv "FRAM_ROUNDTRIP") (str beagle-home "/beagle-lib/private/claims-roundtrip.rkt")))
12+
(def build-all (or (System/getenv "FRAM_BUILD_ALL") (str beagle-home "/bin/beagle-build-all")))
13+
(def code-log (str root "/.fram/code.log"))
14+
(def base-env {"BEAGLE_HOME" beagle-home "FRAM_OUT" (str root "/out") "FRAM_ROUNDTRIP" roundtrip-rkt
15+
"FRAM_RESOLVE" (str root "/chartroom/src/resolve.clj")})
16+
(doseq [p [code-log roundtrip-rkt]] (when-not (.exists (io/file p)) (println "SKIP — missing" p) (System/exit 0)))
17+
(binding [*command-line-args* []] (load-file "cnf_coord_daemon.clj"))
18+
(def flat (str (System/getProperty "java.io.tmpdir") "/edit-min-rename-" (System/nanoTime) ".code.log"))
19+
(io/copy (io/file code-log) (io/file flat))
20+
(defn- port-free? [p] (try (with-open [s (java.net.Socket.)] (.connect s (java.net.InetSocketAddress. "127.0.0.1" (int p)) 300) false) (catch Exception _ true)))
21+
(def port (or (some #(when (port-free? %) %) [8200 8201 8202 8203]) 8200))
22+
(boot-flat! flat)
23+
(def server (future (serve port)))
24+
(Thread/sleep 500)
25+
(defn- shutdown! [] (try (future-cancel server) (catch Throwable _ nil)))
26+
(.addShutdownHook (Runtime/getRuntime) (Thread. shutdown!))
27+
(println "daemon up:" (:claims (client port {:op :status})) "claims, port" port)
28+
29+
;; rename the schema-internal helper `replace!` -> `supersede-prior!` (a value def with
30+
;; in-module references — exercises the capture-check + reference-follows-refers_to).
31+
(def t0 (System/nanoTime))
32+
(def resp (client port {:op :edit-min :spec {:op "rename" :module "schema" :old "replace!" :new "supersede-prior!"}}))
33+
(def ms (/ (- (System/nanoTime) t0) 1e6))
34+
(println (format "rename replace! -> supersede-prior!: %s (%.1f ms)" (pr-str resp) ms))
35+
36+
(def out (str (System/getProperty "java.io.tmpdir") "/edit-min-rename-out-" (System/nanoTime) ".bclj"))
37+
(proc/shell {:continue true :extra-env base-env :err :string} "bb" "-cp" "out" "bin/fram-render-code" "schema" "--log" flat "--out" out)
38+
(def txt (when (.exists (io/file out)) (slurp out)))
39+
(def renamed (boolean (and txt (str/includes? txt "supersede-prior!") (not (str/includes? txt "replace!")))))
40+
;; recompile
41+
(def work (str (System/getProperty "java.io.tmpdir") "/edit-min-rename-rc-" (System/nanoTime)))
42+
(def src (str work "/src")) (.mkdirs (io/file src)) (def bout (str work "/out")) (.mkdirs (io/file bout))
43+
(when txt (spit (str src "/schema.bclj") txt))
44+
(def br (when txt (proc/sh {:out :string :err :string} build-all src "--out" bout)))
45+
(def recompiles (boolean (and br (re-find #"\b1 built, 0 error\(s\)" (str (:out br) (:err br))))))
46+
(shutdown!)
47+
(println "\n=== Build B — rename on the minimal-op path (refers_to-dependent verb) ===")
48+
(println " rename ok :" (boolean (:ok resp)) (str "(" (:ops resp) " ops)"))
49+
(println " new name in render :" renamed)
50+
(println " recompiles 1/0 :" recompiles)
51+
(if (and (:ok resp) renamed recompiles)
52+
(do (println "\nPASS — rename works with the whole-corpus walk OFF (inherited refers_to drives the capture-check + reference following).") (System/exit 0))
53+
(do (println "\nFAIL") (System/exit 1)))

0 commit comments

Comments
 (0)