Skip to content
21 changes: 21 additions & 0 deletions .claude/board/entries/2026-10-12-sag-reprojection-defects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 2026-10-12 — D-SAG-1R — SAG reprojection defects A and B, found by #1510

**Status:** In PR. **Found by:** #1510's audit of #1509.

A: `SoftwareGraph::link` deduplicated on `(src, rel, dst, tier)`, so a fact two
methods share (the claim behind a field both read) belonged to whichever wrote
it first. When that method stopped reading the field, its reprojection
retracted the fact for both. `ctx` is now part of the key; `out` and
`capability` read each fact once.

B: `project_compute` never retracted the DAG it replaced. Cyclic then acyclic
kept the `ComputeCycle` refusal and the stale `ComputesFrom` edges. It now
retracts its own class's compute edges and verdict first.

MEASURED (tests/awareness.rs): for every method, reprojecting it with a body
that reads nothing gives the same facts as a fresh projection; three DAG
sequences give the expected edge count and verdict, the other class untouched.
Three disable runs red on the tests that name each guard.

OPEN: C, `certify` accepts any test name. Who may issue a verified receipt is
an operator decision, not a code fix.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

319 entries, 2026-08-06 .. 2026-10-12.
320 entries, 2026-08-06 .. 2026-10-12.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-10-12 | `D-SAG-1` | | [2026-10-12-software-awareness-graph.md](2026-10-12-software-awareness-graph.md) |
| 2026-10-12 | `D-SAG-1R` | SAG reprojection defects A and B, found by #1510 | [2026-10-12-sag-reprojection-defects.md](2026-10-12-sag-reprojection-defects.md) |
| 2026-10-12 | `D-CMH-G1-6` | | [2026-10-12-ordered-conditions-and-extrema.md](2026-10-12-ordered-conditions-and-extrema.md) |
| 2026-10-12 | `opencog-pln-entropy-research-map` | Source inventory and falsifiable PLN/ECAN/Shannon/Moore attention comparison: entropy target, evidence overlap, graph projection, resource fairness | [2026-10-12-opencog-pln-entropy-research-map.md](2026-10-12-opencog-pln-entropy-research-map.md) |
| 2026-10-12 | `nars-self-evidence-literature` | NARS internal trace versus evidence, Jøsang dependent/independent fusion and fission, and the snapshot-stable fold assumption; source-grounded red tests for merged #1496 | [2026-10-12-nars-self-evidence-literature.md](2026-10-12-nars-self-evidence-literature.md) |
Expand Down
77 changes: 64 additions & 13 deletions crates/lance-graph-glove-parity/src/awareness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,12 +518,18 @@ impl SoftwareGraph {
(self.nodes.len() - 1) as u32
}

/// Add an edge unless the same `(src, rel, dst, tier)` is already there.
/// Add an edge unless the same `(src, rel, dst, tier, ctx)` is already
/// there.
///
/// `ctx` is part of the key: a fact two methods contribute (the claim
/// behind a field both read) is one row per method, so a method's
/// reprojection retracts only its own row and the other method's
/// survives. Readers that ask for facts ([`Self::out`]) see one fact.
pub fn link(&mut self, src: u32, rel: Rel, dst: u32, tier: Tier, ctx: u32, prov: u32) {
let dup = self
.edges
.iter()
.any(|e| (e.src, e.rel, e.dst, e.tier) == (src, rel, dst, tier));
.any(|e| (e.src, e.rel, e.dst, e.tier, e.ctx) == (src, rel, dst, tier, ctx));
if !dup {
self.edges.push(Edge {
src,
Expand All @@ -536,17 +542,21 @@ impl SoftwareGraph {
}
}

/// The edges `src REL ?`, any tier.
/// The facts `src REL ?`, any tier, each once however many contexts
/// wrote it, in first-written order.
#[must_use]
pub fn out(&self, src: &str, rel: Rel) -> Vec<(&str, Tier)> {
let Some(s) = self.node(src) else {
return Vec::new();
};
self.edges
.iter()
.filter(|e| e.src == s && e.rel == rel)
.map(|e| (self.nodes[e.dst as usize].ident.as_str(), e.tier))
.collect()
let mut v: Vec<(&str, Tier)> = Vec::new();
for e in self.edges.iter().filter(|e| e.src == s && e.rel == rel) {
let f = (self.nodes[e.dst as usize].ident.as_str(), e.tier);
if !v.contains(&f) {
v.push(f);
}
}
v
}

/// The current body digest of a projected method.
Expand Down Expand Up @@ -715,6 +725,9 @@ impl SoftwareGraph {
None => {}
}
}
// After every replacement verdict is written, so a refusal the new
// bodies reproduce still supports its blocker.
self.prune_unsupported_blockers();
}

/// Record one lowered program of `owner`: its primitives and lowering,
Expand Down Expand Up @@ -798,6 +811,26 @@ impl SoftwareGraph {
/// # Errors
///
/// The refusal identity, when no compiled refusal carries it.
/// Drop every compiled, refusal-sourced `BlockedBy` edge whose refusal
/// no longer has a compiled `RefusedFor`: such a blocker is admitted only
/// on a demonstrated refusal (see [`Self::blocked_by`]), so it is
/// retracted with the last one. Claimed blockers are left alone.
fn prune_unsupported_blockers(&mut self) {
let supported: std::collections::HashSet<u32> = self
.edges
.iter()
.filter(|e| e.rel == Rel::RefusedFor && e.tier == Tier::Compiled)
.map(|e| e.dst)
.collect();
let nodes = &self.nodes;
self.edges.retain(|e| {
e.rel != Rel::BlockedBy
|| e.tier != Tier::Compiled
|| nodes[e.src as usize].kind != Kind::Refusal
|| supported.contains(&e.src)
});
}

pub fn blocked_by(&mut self, refusal: &str, capability: &str, prov: u32) -> Result<(), String> {
let r = self
.node(refusal)
Expand Down Expand Up @@ -825,6 +858,20 @@ impl SoftwareGraph {
prov: u32,
) {
let class = self.intern(Kind::Class, format!("class:{concept:#06x}"));
// A class's DAG projection replaces its earlier one: its edges and
// its cycle verdict described the DAG it had then. Other facts
// written in the class's context (a note from it) are kept.
let cycle = self.node("refusal:ComputeCycle");
// Only the rows this projection wrote are retracted: derived
// `ComputesFrom` edges run field -> field, so a claim made from the
// class itself (`src == ctx`) or at another tier is kept.
self.edges.retain(|e| {
e.ctx != class
|| !((e.rel == Rel::ComputesFrom && e.tier == Tier::Derived && e.src != e.ctx)
|| (e.rel == Rel::RefusedFor
&& Some(e.dst) == cycle
&& e.tier == Tier::Compiled))
});
Comment thread
AdaWorldAPI marked this conversation as resolved.
let name = |p: u8| {
basis
.get(usize::from(p))
Expand All @@ -842,6 +889,7 @@ impl SoftwareGraph {
let r = self.intern(Kind::Refusal, "refusal:ComputeCycle".into());
self.link(class, Rel::RefusedFor, r, Tier::Compiled, class, prov);
}
self.prune_unsupported_blockers();
}

/// Accept a certificate when the body and seal it names are current.
Expand Down Expand Up @@ -1017,11 +1065,14 @@ impl SoftwareGraph {
return CapStatus::Unknown;
};
let into = |p: &dyn Fn(&Edge) -> bool| -> Vec<String> {
self.edges
.iter()
.filter(|e| e.dst == c && p(e))
.map(|e| self.nodes[e.src as usize].ident.clone())
.collect()
let mut v: Vec<String> = Vec::new();
for e in self.edges.iter().filter(|e| e.dst == c && p(e)) {
let s = &self.nodes[e.src as usize].ident;
if !v.contains(s) {
v.push(s.clone());
}
}
v
};
if !into(&|e| e.rel == Rel::UsesPrimitive && e.tier == Tier::Compiled).is_empty() {
return CapStatus::Shipped;
Expand Down
Loading
Loading