From 38c5cdf09c741fb26049fb1b7d33590cc115336c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:09:10 +0000 Subject: [PATCH 1/7] glove-parity: D-SAG-1 software-awareness graph over prepared methods A projection of a MethodTable and its specs into typed node/edge rows: class, method, field (concept + seal + name), condition and fold (under reading), law, lowered programs, their mask-risc primitives and lowering, compiler refusals, claim grades, compute edges, capabilities, tests. Each edge carries its tier (declared / derived / compiled / verified / hypothesis), the method it was written within, and provenance (file, revision, seal, reading). The edge table implements quack's Binder, so dependency questions are Drafts that bind, lower and run on mask-risc: one program, ScatterOrU32 to the owning method as the demanded result. Structural verdicts are the compiler's (MethodTable::prepare, compute_dag_is_acyclic); the graph records them and adds no competing rule. Tests (16) cover the projection of #1504's billable_hours, query A (dependency), B (refusals, incl. the hypothesized duration claim), C (capability status: shipped only from a compiled program; demonstrated only from a compiled refusal), D (shared predicates and per-fold rescans), H1 (fuse predicate into MaskedSumI32, NULL obligation from the program's gate planes), falsifiers 1-10, and a transparent MDL measure with a label-vs-identity counterexample. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .../lance-graph-glove-parity/src/awareness.rs | 1255 +++++++++++++++++ crates/lance-graph-glove-parity/src/lib.rs | 1 + .../tests/awareness.rs | 950 +++++++++++++ 3 files changed, 2206 insertions(+) create mode 100644 crates/lance-graph-glove-parity/src/awareness.rs create mode 100644 crates/lance-graph-glove-parity/tests/awareness.rs diff --git a/crates/lance-graph-glove-parity/src/awareness.rs b/crates/lance-graph-glove-parity/src/awareness.rs new file mode 100644 index 000000000..8a3484e41 --- /dev/null +++ b/crates/lance-graph-glove-parity/src/awareness.rs @@ -0,0 +1,1255 @@ +//! D-SAG-1: a software-awareness graph — the methods of a concept, their +//! compiled programs and the evidence about them, as typed graph rows queried +//! through Quack. +//! +//! A [`SoftwareGraph`] is a PROJECTION. It is rebuilt from a [`MethodTable`] +//! and the specs it was prepared from; it is never an authority the compiler +//! reads. Every edge says how it is known ([`Tier`]) and where it came from +//! ([`Provenance`]): +//! +//! | tier | written by | example | +//! |---|---|---| +//! | [`Tier::Declared`] | the spec as written | `billable_hours READS_FIELD duration` | +//! | [`Tier::Derived`] | source read at projection (claims, compute edges) | `duration DEPENDS_ON_CLAIM hypothesized` | +//! | [`Tier::Compiled`] | `MethodTable::prepare` and `lower` | `program USES_PRIMITIVE MaskedSumI32` | +//! | [`Tier::Verified`] | an executed oracle, under a [`Certificate`] | `billable_hours VERIFIED_BY …` | +//! | [`Tier::Hypothesis`] | a person or an analysis, unproven | `iam_reconcile MAY_REQUIRE FirstOfRun` | +//! +//! Validation is the compiler's: a refusal is the [`PrepareError`] the table +//! holds, recorded as a [`Rel::RefusedFor`] edge. The graph adds no rule that +//! could disagree with it. +//! +//! The edge table is a Quack table: [`SoftwareGraph`] implements [`Binder`], +//! so a dependency question is a `Draft` that binds, lowers and runs on +//! mask-risc like any other query (see [`SoftwareGraph::methods_using`]). +//! Nothing here runs on a method's warm path. +//! +//! Plan: `.claude/plans/software-awareness-graph-v1.md`. +use std::fmt::Write as _; + +use lance_graph_contract::class_view::{compute_dag_is_acyclic, ComputeEdge}; +use lance_graph_mask_risc::{ + execute_into, materialize_rows, words_for, ExecError, Foreign, LaneRef, Lowering, MaskOp, + Operand, Planes, Pred, Program, Scratch, Terminal, +}; +use lance_graph_quack::bind::{BindError, Binder, BoundField, Draft, FieldKind, TableId}; +use lance_graph_quack::{lower, Agg, Col, LowerError, Mask, Query}; + +use crate::basis::{FieldMap, Grade}; +use crate::method::{ + Cond, Fold, Law, MethodBody, MethodKey, MethodSpec, MethodTable, PrepareError, ReadingId, + SchemaSeal, +}; + +/// What a node is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Kind { + Class, + Method, + /// A canonical field of one concept under one schema seal. + Field, + /// One condition of a body, under its reading. + Cond, + /// One fold of a body, under its reading. + Fold, + Law, + /// One lowered program of a prepared method. + Program, + /// A mask-risc op or terminal kind. + Primitive, + /// How mask-risc executes a program ([`Program::lowering`]). + Lowering, + /// A compiler refusal, by variant. + Refusal, + /// A claim grade on a field. + Claim, + /// A reusable operation, shipped or not. + Capability, + /// An executed check. + Test, +} + +/// What an edge says. Directions are fixed: `src REL dst`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Rel { + /// class → method. + HasMethod, + /// method → field. + ReadsField, + /// method → cond. + Filters, + /// method → fold. + Folds, + /// method → law. + FinishedBy, + /// method → program. + LowersTo, + /// program → primitive (compiled), or method → primitive (a claim). + UsesPrimitive, + /// program → lowering. + LowersAs, + /// method or class → refusal. + RefusedFor, + /// field → claim. + DependsOnClaim, + /// field → field (`target` recomputed from `input`). + ComputesFrom, + /// method or refusal → capability: the capability would admit it. + BlockedBy, + /// method → capability, unproven. + MayRequire, + /// method → test, under a certificate. + VerifiedBy, +} + +/// How an edge is known. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Tier { + Declared, + Derived, + Compiled, + Verified, + Hypothesis, +} + +/// Where a projection's facts came from. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Provenance { + /// The file the specs are declared in. + pub source: String, + /// The source revision (a commit, or `worktree`). + pub rev: String, + pub seal: SchemaSeal, + pub reading: ReadingId, +} + +/// One node: a kind and an identity string. Labels are display text and +/// never part of the identity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Node { + pub kind: Kind, + pub ident: String, + pub label: Option, +} + +/// One edge row. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Edge { + pub src: u32, + pub rel: Rel, + pub dst: u32, + pub tier: Tier, + /// The method (or class) whose projection wrote the edge: the hop a + /// dependency query lands on. + pub ctx: u32, + pub prov: u32, +} + +/// Proof that a method's current body ran against an oracle. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Certificate { + pub method: MethodKey, + /// [`SoftwareGraph::digest`] of the body when the oracle ran. + pub digest: u64, + pub seal: SchemaSeal, + pub test: String, +} + +/// Why a certificate is not accepted. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CertError { + UnknownMethod(MethodKey), + /// The body changed since the oracle ran. + StaleBody { + method: MethodKey, + }, + /// The schema changed since the oracle ran. + StaleSeal { + method: MethodKey, + }, +} + +/// A dependency query that did not run. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueryError { + Bind(BindError), + Lower(LowerError), + Exec(ExecError), +} + +/// Whether a reusable operation exists, from the graph's evidence alone. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapStatus { + /// A compiled program contains it. + Shipped, + /// Not shipped; a compiler refusal names it as the missing piece. + Demonstrated { + refusals: Vec, + }, + /// Not shipped; only declared or hypothesized needs. + Claimed { + by: Vec, + }, + Unknown, +} + +/// A program that names a primitive its claim edges do not match. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClaimMismatch { + pub method: String, + pub primitive: String, +} + +/// A structural optimization candidate. Never a rewrite. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Candidate { + pub kind: CandidateKind, + /// The method nodes that would change. + pub methods: Vec, + /// What must hold for the rewrite to be legal, each open or discharged. + pub obligations: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CandidateKind { + /// The same conditions in several methods: factor one predicate. + SharedPredicate { conds: Vec }, + /// One method's programs evaluate the same selection more than once. + SharedSelection { programs: Vec }, + /// A tiled program whose selection feeds a masked sum: H1, fuse the + /// predicate into the fold. + FusePredicateIntoSum { program: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Obligation { + pub what: &'static str, + /// `Some(reason)` when the graph's evidence discharges it. + pub discharged: Option, +} + +/// The CPU a measurement ran on: the compiled target and the host feature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CpuTier { + pub target: String, + pub avx512f: bool, +} + +/// One timing of one program on one dataset and tier. +#[derive(Debug, Clone, PartialEq)] +pub struct Measurement { + pub program: String, + pub rows: usize, + pub tier: CpuTier, + pub median_ns: f64, +} + +/// A measurement refused for the tier it was asked about. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CostError { + Unmeasured, + TierMismatch { measured: CpuTier }, +} + +/// One workload's before/after timing. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Workload { + pub before_ns: f64, + pub after_ns: f64, +} + +/// What a set of workloads says about a transformation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Verdict { + Unmeasured, + Improves, + Regresses, + /// Faster on some, slower on others: not an improvement. + Mixed, +} + +/// Faster everywhere, slower everywhere, or mixed. A tie is not faster. +#[must_use] +pub fn verdict(w: &[Workload]) -> Verdict { + if w.is_empty() { + return Verdict::Unmeasured; + } + let up = w.iter().filter(|x| x.after_ns < x.before_ns).count(); + let down = w.iter().filter(|x| x.after_ns > x.before_ns).count(); + match (up, down) { + (_, 0) if up == w.len() => Verdict::Improves, + (0, _) if down == w.len() => Verdict::Regresses, + _ => Verdict::Mixed, + } +} + +/// The software-awareness graph. +#[derive(Debug, Default, Clone)] +pub struct SoftwareGraph { + nodes: Vec, + edges: Vec, + provs: Vec, + /// `(method, body digest, seal)` of every projected spec. + digests: Vec<(MethodKey, u64, SchemaSeal)>, + measurements: Vec, +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().fold(String::new(), |mut s, b| { + let _ = write!(s, "{b:02x}"); + s + }) +} + +/// The identity of a method node. +#[must_use] +pub fn method_ident(key: MethodKey) -> String { + format!("method:{}", hex(&key.0)) +} + +/// The identity of a field node: concept, seal and canonical name. Two +/// concepts that share a field name have two field nodes. +#[must_use] +pub fn field_ident(concept: u16, seal: SchemaSeal, name: &str) -> String { + format!("field:{concept:#06x}/{:016x}/{name}", seal.0) +} + +/// The primitive kind of a mask op, by variant. +#[must_use] +pub fn op_name(op: &MaskOp) -> String { + match op { + MaskOp::Pred { pred, .. } => format!("Pred.{}", pred_name(pred)), + MaskOp::And { .. } => "And".into(), + MaskOp::Or { .. } => "Or".into(), + MaskOp::Xor { .. } => "Xor".into(), + MaskOp::AndNot { .. } => "AndNot".into(), + MaskOp::Not { .. } => "Not".into(), + MaskOp::Ternlog { .. } => "Ternlog".into(), + MaskOp::Gather { .. } => "Gather".into(), + } +} + +fn pred_name(p: &Pred) -> &'static str { + match p { + Pred::GtI32 { .. } => "GtI32", + Pred::LtI32 { .. } => "LtI32", + Pred::GeI32 { .. } => "GeI32", + Pred::LeI32 { .. } => "LeI32", + Pred::EqI32 { .. } => "EqI32", + Pred::NeI32 { .. } => "NeI32", + Pred::EqU32 { .. } => "EqU32", + Pred::NeU32 { .. } => "NeU32", + Pred::MatchU32 { .. } => "MatchU32", + Pred::MatchU64 { .. } => "MatchU64", + Pred::EqU32Via { .. } => "EqU32Via", + Pred::Range { .. } => "Range", + Pred::EqU32Strided { .. } => "EqU32Strided", + Pred::NeU32Strided { .. } => "NeU32Strided", + Pred::MatchFacetStrided { .. } => "MatchFacetStrided", + Pred::MatchFacet16Strided { .. } => "MatchFacet16Strided", + } +} + +/// The terminal kind, by variant. +#[must_use] +pub fn terminal_name(t: &Terminal) -> &'static str { + match t { + Terminal::Count { .. } => "Count", + Terminal::Any { .. } => "Any", + Terminal::All { .. } => "All", + Terminal::MaskedSumI32 { .. } => "MaskedSumI32", + Terminal::MaskedMinI32 { .. } => "MaskedMinI32", + Terminal::MaskedMaxI32 { .. } => "MaskedMaxI32", + Terminal::MaskedStridedGroupSum { .. } => "MaskedStridedGroupSum", + Terminal::BlendI32 { .. } => "BlendI32", + Terminal::Keep { .. } => "Keep", + Terminal::ScatterOrU32 { .. } => "ScatterOrU32", + Terminal::ScatterCountU32 { .. } => "ScatterCountU32", + Terminal::CountKeyRunsU32 { .. } => "CountKeyRunsU32", + Terminal::GroupSumI32 { .. } => "GroupSumI32", + Terminal::GroupSumViaI32 { .. } => "GroupSumViaI32", + Terminal::GroupReduce { .. } => "GroupReduce", + Terminal::GroupPowerSumsI32 { .. } => "GroupPowerSumsI32", + Terminal::GroupCrossPowerSumsI32 { .. } => "GroupCrossPowerSumsI32", + } +} + +fn lowering_name(l: Lowering) -> &'static str { + match l { + Lowering::Range(_) => "Range", + Lowering::Ternlog(_) => "Ternlog", + Lowering::TernlogKeep(_) => "TernlogKeep", + Lowering::Tern2(_) => "Tern2", + Lowering::Tern3(_) => "Tern3", + Lowering::Tiled => "Tiled", + } +} + +fn refusal_name(e: &PrepareError) -> String { + let d = format!("{e:?}"); + d.split(|c: char| !c.is_alphanumeric()) + .next() + .unwrap_or_default() + .to_owned() +} + +/// FNV-1a over a spec's rendering. Stable within one build: a toolchain +/// change makes every certificate stale (never falsely fresh). +fn digest(spec: &MethodSpec<'_>) -> u64 { + let mut h = 0xcbf2_9ce4_8422_2325u64; + for b in format!("{spec:?}").bytes() { + h = (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3); + } + h +} + +const EDGE_TABLE: &str = "edges"; +const SRC: u16 = 0; +const REL: u16 = 1; +const DST: u16 = 2; +const TIER: u16 = 3; +const CTX: u16 = 4; + +const RELS: [(Rel, &str); 14] = [ + (Rel::HasMethod, "has_method"), + (Rel::ReadsField, "reads_field"), + (Rel::Filters, "filters"), + (Rel::Folds, "folds"), + (Rel::FinishedBy, "finished_by"), + (Rel::LowersTo, "lowers_to"), + (Rel::UsesPrimitive, "uses_primitive"), + (Rel::LowersAs, "lowers_as"), + (Rel::RefusedFor, "refused_for"), + (Rel::DependsOnClaim, "depends_on_claim"), + (Rel::ComputesFrom, "computes_from"), + (Rel::BlockedBy, "blocked_by"), + (Rel::MayRequire, "may_require"), + (Rel::VerifiedBy, "verified_by"), +]; + +const TIERS: [(Tier, &str); 5] = [ + (Tier::Declared, "declared"), + (Tier::Derived, "derived"), + (Tier::Compiled, "compiled"), + (Tier::Verified, "verified"), + (Tier::Hypothesis, "hypothesis"), +]; + +fn rel_code(r: Rel) -> u32 { + RELS.iter() + .position(|(x, _)| *x == r) + .expect("every rel is listed") as u32 +} + +fn tier_code(t: Tier) -> u32 { + TIERS + .iter() + .position(|(x, _)| *x == t) + .expect("every tier is listed") as u32 +} + +impl SoftwareGraph { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn nodes(&self) -> &[Node] { + &self.nodes + } + + #[must_use] + pub fn edges(&self) -> &[Edge] { + &self.edges + } + + #[must_use] + pub fn provenance(&self, edge: &Edge) -> &Provenance { + &self.provs[edge.prov as usize] + } + + /// The node with this identity. + #[must_use] + pub fn node(&self, ident: &str) -> Option { + self.nodes + .iter() + .position(|n| n.ident == ident) + .map(|i| i as u32) + } + + fn intern(&mut self, kind: Kind, ident: String) -> u32 { + if let Some(i) = self.node(&ident) { + return i; + } + self.nodes.push(Node { + kind, + ident, + label: None, + }); + (self.nodes.len() - 1) as u32 + } + + /// Add an edge unless the same `(src, rel, dst, tier)` is already there. + 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)); + if !dup { + self.edges.push(Edge { + src, + rel, + dst, + tier, + ctx, + prov, + }); + } + } + + /// The edges `src REL ?`, any tier. + #[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() + } + + /// The current body digest of a projected method. + #[must_use] + pub fn digest(&self, key: MethodKey) -> Option { + self.digests + .iter() + .rev() + .find(|(k, ..)| *k == key) + .map(|(_, d, _)| *d) + } + + /// Project a prepared table and the specs it was prepared from. + /// + /// `claims` are the glove's claims, read so that a field's grade is a + /// node (a method whose field is `Hypothesized` is refused, and the + /// graph says why). A spec's identity facts are `Declared`, the claims + /// `Derived`, the table's verdicts and programs `Compiled`. + pub fn project( + &mut self, + table: &MethodTable, + specs: &[(&str, MethodSpec<'_>)], + claims: &[FieldMap], + source: &str, + rev: &str, + ) { + for (label, spec) in specs { + let prov = self.provs.len() as u32; + self.provs.push(Provenance { + source: source.into(), + rev: rev.into(), + seal: spec.schema, + reading: spec.reading, + }); + self.digests.push((spec.key, digest(spec), spec.schema)); + let class = self.intern(Kind::Class, format!("class:{:#06x}", spec.concept)); + let m = self.intern(Kind::Method, method_ident(spec.key)); + self.nodes[m as usize].label = Some((*label).to_owned()); + self.link(class, Rel::HasMethod, m, Tier::Declared, m, prov); + let r = spec.reading.0; + let field = |g: &mut Self, name: &str| { + let f = g.intern(Kind::Field, field_ident(spec.concept, spec.schema, name)); + g.link(m, Rel::ReadsField, f, Tier::Declared, m, prov); + if let Some(c) = claims.iter().find(|c| c.canonical == name) { + let grade = match c.grade { + Grade::Exact => "exact", + Grade::Converted => "converted", + Grade::Hypothesized => "hypothesized", + }; + let claim = g.intern(Kind::Claim, format!("claim:{}:{grade}", c.native)); + g.link(f, Rel::DependsOnClaim, claim, Tier::Derived, m, prov); + } + g.nodes[f as usize].ident.clone() + }; + if let MethodBody::Query { filter, folds, law } = spec.body { + for c in filter { + let (name, op, lit) = match *c { + Cond::Eq { field, literal } => (field, "=", literal), + Cond::Ne { field, literal } => (field, "!=", literal), + }; + let f = field(self, name); + let n = self.intern(Kind::Cond, format!("cond:{f}{op}{lit}@r{r}")); + self.link(m, Rel::Filters, n, Tier::Declared, m, prov); + } + for fold in folds { + let ident = match *fold { + Fold::Count => "fold:count(*)".to_owned(), + Fold::Sum(f) => format!("fold:sum({})", field(self, f)), + Fold::GroupSum { key, val, groups } => format!( + "fold:group_sum({},{},{groups})", + field(self, key), + field(self, val) + ), + }; + let n = self.intern(Kind::Fold, format!("{ident}@r{r}")); + self.link(m, Rel::Folds, n, Tier::Declared, m, prov); + } + let law = match law { + Law::Values => "law:values".to_owned(), + Law::NarsRevision { + independent, + scale, + horizon, + } => format!("law:nars_revision({independent},{scale},{horizon})"), + }; + let n = self.intern(Kind::Law, law); + self.link(m, Rel::FinishedBy, n, Tier::Declared, m, prov); + } + match table.get(spec.key) { + Some(Ok(p)) => { + for (i, program) in p.programs().iter().enumerate() { + let pid = format!("program:{}#{i}", hex(&spec.key.0)); + self.add_program(m, &pid, program, prov); + } + } + Some(Err(e)) => { + let n = self.intern(Kind::Refusal, format!("refusal:{}", refusal_name(e))); + self.link(m, Rel::RefusedFor, n, Tier::Compiled, m, prov); + } + None => {} + } + } + } + + /// Record one lowered program of `owner`: its primitives and lowering, + /// read from the program itself. + pub fn add_program(&mut self, owner: u32, ident: &str, program: &Program, prov: u32) { + let p = self.intern(Kind::Program, ident.to_owned()); + self.link(owner, Rel::LowersTo, p, Tier::Compiled, owner, prov); + let names = program + .ops + .iter() + .map(op_name) + .chain([terminal_name(&program.terminal).to_owned()]); + for name in names { + let n = self.intern(Kind::Primitive, format!("prim:{name}")); + self.link(p, Rel::UsesPrimitive, n, Tier::Compiled, owner, prov); + } + let l = self.intern( + Kind::Lowering, + format!("lowering:{}", lowering_name(program.lowering())), + ); + self.link(p, Rel::LowersAs, l, Tier::Compiled, owner, prov); + } + + /// A provenance row for facts not projected from a table. + pub fn note(&mut self, source: &str, rev: &str) -> u32 { + self.provs.push(Provenance { + source: source.into(), + rev: rev.into(), + seal: SchemaSeal(0), + reading: ReadingId(0), + }); + (self.provs.len() - 1) as u32 + } + + /// Record a need, or a claim, by identity: `method REL capability` at + /// `tier`. Recording never ships anything (see [`Self::capability`]). + pub fn claim(&mut self, method: &str, rel: Rel, target: &str, tier: Tier, prov: u32) { + let kind = if target.starts_with("prim:") { + Kind::Primitive + } else { + Kind::Capability + }; + let m = self.intern(Kind::Method, method.to_owned()); + let c = self.intern(kind, target.to_owned()); + self.link(m, rel, c, tier, m, prov); + } + + /// A method node by identity, for programs not prepared by a table. + pub fn add_method(&mut self, ident: &str) -> u32 { + self.intern(Kind::Method, ident.to_owned()) + } + + /// Record that `capability` would admit what a compiler refusal + /// rejects. Only a refusal the compiler actually recorded (a compiled + /// `RefusedFor` edge) can be named: a gap is demonstrated by a refusal, + /// never by a note. + /// + /// # Errors + /// + /// The refusal identity, when no compiled refusal carries it. + pub fn blocked_by(&mut self, refusal: &str, capability: &str, prov: u32) -> Result<(), String> { + let r = self + .node(refusal) + .filter(|&r| { + self.edges + .iter() + .any(|e| e.dst == r && e.rel == Rel::RefusedFor && e.tier == Tier::Compiled) + }) + .ok_or_else(|| refusal.to_owned())?; + let c = self.intern(Kind::Capability, capability.to_owned()); + self.link(r, Rel::BlockedBy, c, Tier::Compiled, r, prov); + Ok(()) + } + + /// Project a class's compute DAG and record the contract's verdict. + /// + /// The cycle check is `class_view::compute_dag_is_acyclic`, run on the + /// declaration; the graph only records what it said. + pub fn project_compute( + &mut self, + concept: u16, + seal: SchemaSeal, + basis: &[String], + edges: &[ComputeEdge], + prov: u32, + ) { + let class = self.intern(Kind::Class, format!("class:{concept:#06x}")); + let name = |p: u8| { + basis + .get(usize::from(p)) + .cloned() + .unwrap_or(format!("#{p}")) + }; + for e in edges { + let t = self.intern(Kind::Field, field_ident(concept, seal, &name(e.target))); + for &i in e.inputs { + let f = self.intern(Kind::Field, field_ident(concept, seal, &name(i))); + self.link(t, Rel::ComputesFrom, f, Tier::Derived, class, prov); + } + } + if !compute_dag_is_acyclic(edges) { + let r = self.intern(Kind::Refusal, "refusal:ComputeCycle".into()); + self.link(class, Rel::RefusedFor, r, Tier::Compiled, class, prov); + } + } + + /// Accept a certificate when the body and seal it names are current. + /// + /// # Errors + /// + /// [`CertError`] when the method is unknown or changed since. + pub fn certify(&mut self, cert: &Certificate, prov: u32) -> Result<(), CertError> { + let Some(&(_, d, s)) = self.digests.iter().rev().find(|(k, ..)| *k == cert.method) else { + return Err(CertError::UnknownMethod(cert.method)); + }; + if d != cert.digest { + return Err(CertError::StaleBody { + method: cert.method, + }); + } + if s != cert.seal { + return Err(CertError::StaleSeal { + method: cert.method, + }); + } + let m = self.intern(Kind::Method, method_ident(cert.method)); + let t = self.intern(Kind::Test, format!("test:{}", cert.test)); + self.link(m, Rel::VerifiedBy, t, Tier::Verified, m, prov); + Ok(()) + } + + /// Record a measurement. + pub fn measure(&mut self, m: Measurement) { + self.measurements.push(m); + } + + /// The measured cost of `program` on `tier`. + /// + /// # Errors + /// + /// [`CostError::TierMismatch`] when it was measured only on another + /// tier; a number from one CPU is never reported for another. + pub fn cost(&self, program: &str, rows: usize, tier: &CpuTier) -> Result { + let mut other = None; + for m in self + .measurements + .iter() + .filter(|m| m.program == program && m.rows == rows) + { + if &m.tier == tier { + return Ok(m.median_ns); + } + other = Some(m.tier.clone()); + } + Err( + other.map_or(CostError::Unmeasured, |measured| CostError::TierMismatch { + measured, + }), + ) + } + + // ---- queries over the edge table, through Quack ------------------- + + fn columns(&self) -> ([Vec; 5], Vec) { + let n = self.edges.len(); + let mut cols: [Vec; 5] = Default::default(); + for e in &self.edges { + cols[SRC as usize].push(e.src); + cols[REL as usize].push(rel_code(e.rel)); + cols[DST as usize].push(e.dst); + cols[TIER as usize].push(tier_code(e.tier)); + cols[CTX as usize].push(e.ctx); + } + let mut live = vec![u64::MAX; words_for(n)]; + if !n.is_multiple_of(64) { + if let Some(last) = live.last_mut() { + *last = (1u64 << (n % 64)) - 1; + } + } + (cols, live) + } + + /// Run `draft` over the edge table and hop each surviving edge to its + /// `ctx` node: one Quack program, `ScatterOrU32` as the demanded result. + /// + /// # Errors + /// + /// [`QueryError`] from binding, lowering or execution. + pub fn hop_to_ctx(&self, draft: &Draft) -> Result, QueryError> { + let bound = draft.bind(self).map_err(QueryError::Bind)?; + let q = Query { + filter: bound.filter, + agg: Agg::ScatterOrU32 { + fk: Col(CTX), + out_rows: self.nodes.len() as u32, + }, + }; + let program = lower(&q).map_err(QueryError::Lower)?; + let (cols, live) = self.columns(); + let lanes: Vec> = cols.iter().map(|c| LaneRef::U32(c)).collect(); + let planes = Planes { + n_rows: self.edges.len(), + masks: &[live.as_slice()], + lanes: &lanes, + }; + let mut scratch = + Scratch::for_program(&program, planes.n_rows).map_err(QueryError::Exec)?; + let mut out = vec![0u64; words_for(self.nodes.len())]; + execute_into( + &program, + &planes, + &Foreign::NONE, + &mut scratch, + lance_graph_mask_risc::Out::Mask(&mut out), + ) + .map_err(QueryError::Exec)?; + Ok(materialize_rows(&out, self.nodes.len()) + .into_iter() + .map(|i| self.nodes[i].ident.as_str()) + .collect()) + } + + /// Query A: the methods whose compiled programs use `primitive`. + /// + /// # Errors + /// + /// [`QueryError`]; an unknown primitive is `BindError::UnknownValue`. + pub fn methods_using(&self, primitive: &str) -> Result, QueryError> { + self.hop_to_ctx( + &lance_graph_quack::bind::table(EDGE_TABLE) + .where_eq("rel", "uses_primitive") + .where_eq("tier", "compiled") + .where_eq("dst", format!("prim:{primitive}").as_str()), + ) + } + + /// Query B: the methods (or classes) the compiler refused, for `reason`. + /// + /// # Errors + /// + /// As [`Self::hop_to_ctx`]. + pub fn refused_for(&self, reason: &str) -> Result, QueryError> { + self.hop_to_ctx( + &lance_graph_quack::bind::table(EDGE_TABLE) + .where_eq("rel", "refused_for") + .where_eq("dst", format!("refusal:{reason}").as_str()), + ) + } + + /// Falsifier 8: every primitive a method is CLAIMED to use (any tier but + /// compiled) must appear in one of its compiled programs. + #[must_use] + pub fn primitive_claim_mismatches(&self) -> Vec { + self.edges + .iter() + .filter(|e| e.rel == Rel::UsesPrimitive && e.tier != Tier::Compiled) + .filter(|claim| { + !self.edges.iter().any(|c| { + c.rel == Rel::UsesPrimitive + && c.tier == Tier::Compiled + && c.ctx == claim.src + && c.dst == claim.dst + }) + }) + .map(|e| ClaimMismatch { + method: self.nodes[e.src as usize].ident.clone(), + primitive: self.nodes[e.dst as usize].ident.clone(), + }) + .collect() + } + + /// Falsifier 9: a capability is shipped only when a COMPILED program + /// uses it; a declared need never ships it. + #[must_use] + pub fn capability(&self, ident: &str) -> CapStatus { + let Some(c) = self.node(ident) else { + return CapStatus::Unknown; + }; + let into = |p: &dyn Fn(&Edge) -> bool| -> Vec { + self.edges + .iter() + .filter(|e| e.dst == c && p(e)) + .map(|e| self.nodes[e.src as usize].ident.clone()) + .collect() + }; + if !into(&|e| e.rel == Rel::UsesPrimitive && e.tier == Tier::Compiled).is_empty() { + return CapStatus::Shipped; + } + let refusals = + into(&|e| e.rel == Rel::BlockedBy && self.nodes[e.src as usize].kind == Kind::Refusal); + if !refusals.is_empty() { + return CapStatus::Demonstrated { refusals }; + } + let by = into(&|_| true); + if by.is_empty() { + CapStatus::Unknown + } else { + CapStatus::Claimed { by } + } + } + + fn methods(&self) -> impl Iterator + '_ { + (0..self.nodes.len() as u32).filter(|&i| self.nodes[i as usize].kind == Kind::Method) + } + + fn targets(&self, src: u32, rel: Rel) -> Vec { + let mut v: Vec = self + .edges + .iter() + .filter(|e| e.src == src && e.rel == rel) + .map(|e| e.dst) + .collect(); + v.sort_unstable(); + v.dedup(); + v + } + + /// Query D (a): condition sets shared by two or more prepared methods. + /// Conditions are compared by identity (field under concept and seal, + /// operator, literal, reading), never by label. + #[must_use] + pub fn shared_predicates(&self) -> Vec { + let prepared: Vec = self + .methods() + .filter(|&m| !self.targets(m, Rel::LowersTo).is_empty()) + .collect(); + let mut out: Vec = Vec::new(); + for (i, &a) in prepared.iter().enumerate() { + let ca = self.targets(a, Rel::Filters); + if ca.is_empty() { + continue; + } + let mut group = vec![a]; + for &b in &prepared[i + 1..] { + if self.targets(b, Rel::Filters) == ca { + group.push(b); + } + } + let conds: Vec = ca + .iter() + .map(|&c| self.nodes[c as usize].ident.clone()) + .collect(); + let seen = out.iter().any( + |c| matches!(&c.kind, CandidateKind::SharedPredicate { conds: x } if *x == conds), + ); + if group.len() > 1 && !seen { + out.push(Candidate { + kind: CandidateKind::SharedPredicate { conds }, + methods: group + .iter() + .map(|&m| self.nodes[m as usize].ident.clone()) + .collect(), + obligations: vec![ + Obligation { + what: "same concept, seal, reading and effect", + discharged: Some( + "condition identities include concept, seal and reading".into(), + ), + }, + Obligation { + what: "the predicate method lowers to the same program", + discharged: None, + }, + ], + }); + } + } + out + } + + /// Query D (b): a method whose programs carry identical selection ops, + /// i.e. the same population scanned once per fold. + #[must_use] + pub fn shared_selections(&self, table: &MethodTable) -> Vec { + let mut out = Vec::new(); + for m in self.methods() { + let ident = &self.nodes[m as usize].ident; + let Some(key) = parse_key(ident) else { + continue; + }; + let Some(Ok(p)) = table.get(key) else { + continue; + }; + let progs = p.programs(); + if progs.len() > 1 && progs.iter().all(|x| x.ops == progs[0].ops) { + out.push(Candidate { + kind: CandidateKind::SharedSelection { + programs: (0..progs.len()) + .map(|i| format!("program:{}#{i}", hex(&key.0))) + .collect(), + }, + methods: vec![ident.clone()], + obligations: vec![Obligation { + what: "one mask feeding several terminals is a population \ + intermediate the executor forbids; needs a multi-terminal \ + program, not a shared mask", + discharged: None, + }], + }); + } + } + out + } + + /// H1: tiled programs whose selection feeds a `MaskedSumI32`. Each + /// candidate lists what fusing the predicate into the sum must keep. + #[must_use] + pub fn fusion_candidates(&self, programs: &[(String, &Program)]) -> Vec { + programs + .iter() + .filter(|(_, p)| { + matches!(p.terminal, Terminal::MaskedSumI32 { .. }) + && p.lowering() == Lowering::Tiled + }) + .map(|(id, p)| { + let gates = gate_planes(p); + let owner = self + .node(id) + .and_then(|pid| { + self.edges + .iter() + .find(|e| e.dst == pid && e.rel == Rel::LowersTo) + }) + .map(|e| self.nodes[e.src as usize].ident.clone()); + Candidate { + kind: CandidateKind::FusePredicateIntoSum { + program: id.clone(), + }, + methods: owner.into_iter().collect(), + obligations: vec![ + Obligation { + what: "NULL/UNKNOWN: every validity plane the program ANDs must \ + still gate the fused sum", + discharged: (gates.len() <= 1).then(|| { + "the program reads one plane (live); no validity gate".into() + }), + }, + Obligation { + what: "row bound: at most MASKED_SUM_I32_MAX_ROWS rows", + discharged: None, + }, + Obligation { + what: "the fused terminal must exist: FusedFold has only Count/Any", + discharged: None, + }, + ], + } + }) + .collect() + } +} + +/// The resident planes a program reads as gates. +fn gate_planes(p: &Program) -> Vec { + let mut v = Vec::new(); + let mut see = |o: Operand| { + if let Operand::Plane(k) = o { + if !v.contains(&k) { + v.push(k); + } + } + }; + for op in &p.ops { + match *op { + MaskOp::Pred { under, .. } | MaskOp::Gather { under, .. } => { + if let Some(u) = under { + see(u); + } + } + MaskOp::And { a, b, .. } + | MaskOp::Or { a, b, .. } + | MaskOp::Xor { a, b, .. } + | MaskOp::AndNot { a, b, .. } => { + see(a); + see(b); + } + MaskOp::Not { a, .. } => see(a), + MaskOp::Ternlog { a, b, c, .. } => { + see(a); + see(b); + see(c); + } + } + } + v +} + +fn parse_key(ident: &str) -> Option { + let h = ident.strip_prefix("method:")?; + if h.len() != 32 { + return None; + } + let mut k = [0u8; 16]; + for (i, b) in k.iter_mut().enumerate() { + *b = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?; + } + Some(MethodKey(k)) +} + +/// The edge table as a Quack table: `edges(src, rel, dst, tier, ctx)`, every +/// field a code. Node identities, relation and tier names are the codebook; +/// nothing is minted. +impl Binder for SoftwareGraph { + fn table(&self, name: &str) -> Option { + (name == EDGE_TABLE).then_some(TableId(0)) + } + fn live(&self, _: TableId) -> Mask { + Mask(0) + } + fn field(&self, _: TableId, name: &str) -> Option { + let col = match name { + "src" => SRC, + "rel" => REL, + "dst" => DST, + "tier" => TIER, + "ctx" => CTX, + _ => return None, + }; + Some(BoundField { + col: Col(col), + kind: FieldKind::Code, + validity: None, + }) + } + fn code(&self, _: TableId, field: Col, literal: &str) -> Option { + match field.0 { + REL => RELS + .iter() + .position(|(_, n)| *n == literal) + .map(|i| i as u32), + TIER => TIERS + .iter() + .position(|(_, n)| *n == literal) + .map(|i| i as u32), + _ => self.node(literal), + } + } +} + +/// Description length of the methods' declared bodies (D-SAG-1 §5). +/// +/// Transparent, deliberately crude: a body is the sequence of its condition, +/// fold and law tokens. `L(P)` = each atom × `log2(atoms)`, plus each used +/// composite × one `log2(|V|)` reference per atom it is defined over; +/// `L(M|P)` = each method's tokens × `log2(|V|)`, where `V` is the atoms and +/// the used composites. A composite (a factored predicate) replaces its +/// conditions as one token in every method that carries all of them. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Dl { + pub primitives: f64, + pub methods: f64, +} + +impl Dl { + #[must_use] + pub fn total(&self) -> f64 { + self.primitives + self.methods + } +} + +/// `bodies`: each method's token identities. `composites`: factored +/// condition sets. Identities are compared as given; the caller decides +/// whether they are typed identities or bare labels. +#[must_use] +pub fn description_length(bodies: &[Vec], composites: &[Vec]) -> Dl { + let mut atoms: Vec<&String> = bodies.iter().flatten().collect(); + atoms.sort(); + atoms.dedup(); + let atom_bits = (atoms.len().max(2) as f64).log2(); + let rewritten: Vec> = bodies + .iter() + .map(|b| { + let mut b = b.clone(); + for (i, c) in composites.iter().enumerate() { + if c.iter().all(|x| b.contains(x)) { + b.retain(|x| !c.contains(x)); + b.push(format!("composite#{i}")); + } + } + b + }) + .collect(); + // The vocabulary is every token a rewritten body uses, plus every atom a + // used composite is defined over: a composite is spelled in atoms, so + // its atoms stay in the alphabet even when no body names them alone. + let used: Vec = (0..composites.len()) + .filter(|i| { + rewritten + .iter() + .flatten() + .any(|t| *t == format!("composite#{i}")) + }) + .collect(); + let mut vocab: Vec<&String> = rewritten + .iter() + .flatten() + .filter(|t| !t.starts_with("composite#")) + .chain(used.iter().flat_map(|&i| composites[i].iter())) + .collect(); + vocab.sort(); + vocab.dedup(); + let v_bits = ((vocab.len() + used.len()).max(2) as f64).log2(); + // Atoms cost their spelling; a composite costs one reference per atom. + let primitives = vocab.len() as f64 * atom_bits + + used + .iter() + .map(|&i| composites[i].len() as f64 * v_bits) + .sum::(); + let methods = rewritten.iter().map(|b| b.len() as f64 * v_bits).sum(); + Dl { + primitives, + methods, + } +} + +impl SoftwareGraph { + /// Each prepared-or-not method's declared tokens, by identity. + #[must_use] + pub fn bodies(&self) -> Vec<(String, Vec)> { + self.methods() + .map(|m| { + let toks = [Rel::Filters, Rel::Folds, Rel::FinishedBy] + .into_iter() + .flat_map(|r| self.targets(m, r)) + .map(|t| self.nodes[t as usize].ident.clone()) + .collect(); + (self.nodes[m as usize].ident.clone(), toks) + }) + .filter(|(_, t): &(String, Vec)| !t.is_empty()) + .collect() + } +} diff --git a/crates/lance-graph-glove-parity/src/lib.rs b/crates/lance-graph-glove-parity/src/lib.rs index 3ba748ae8..712fb2054 100644 --- a/crates/lance-graph-glove-parity/src/lib.rs +++ b/crates/lance-graph-glove-parity/src/lib.rs @@ -11,6 +11,7 @@ //! glove's own `Binder`, so a `Draft` written in canonical names binds to the //! glove's lanes. This is a FIELD MAPPING (bind time, fields → lanes), not a //! report `pivot()` (plan §C.6.1). +pub mod awareness; pub mod basis; pub mod call; pub mod decl; diff --git a/crates/lance-graph-glove-parity/tests/awareness.rs b/crates/lance-graph-glove-parity/tests/awareness.rs new file mode 100644 index 000000000..37ba13b87 --- /dev/null +++ b/crates/lance-graph-glove-parity/tests/awareness.rs @@ -0,0 +1,950 @@ +//! D-SAG-1: the software-awareness graph over #1504's methods. +//! +//! The methods are #1504's own declarations (`tests/method.rs`), projected +//! with the table that prepared them. Every structural verdict below is the +//! compiler's (`MethodTable::prepare`, `class_view::compute_dag_is_acyclic`); +//! the graph records it and the queries read it back through Quack. +use lance_graph_contract::class_view::ComputeEdge; +use lance_graph_glove_parity::awareness::{ + description_length, field_ident, method_ident, verdict, Candidate, CandidateKind, CapStatus, + CertError, Certificate, CostError, CpuTier, Measurement, Rel, SoftwareGraph, Tier, Verdict, + Workload, +}; +use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; +use lance_graph_glove_parity::method::{ + Answer, Cond, EffectClass, Fold, FoldValue, Frontend, Glove, Law, MethodBody, MethodKey, + MethodSpec, MethodTable, ReadingId, SchemaSeal, Universe, +}; +use lance_graph_mask_risc::{LaneRef, Planes, Program}; +use lance_graph_ogar::ogar_vocab::class_ids::{BILLABLE_WORK_ENTRY, DOCUMENT}; +use lance_graph_quack::bind::{Binder, BoundField, FieldKind, TableId}; +use lance_graph_quack::{lower, Agg, Cmp, Col, Filter, Mask, Query}; +use lance_graph_sap::bind::{billable_code, CatsBatch, BILLING, HOURS}; +use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; +use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; + +const COUNT_BILLABLE: MethodKey = MethodKey([0xB1; 16]); +const COUNT_BY_DURATION: MethodKey = MethodKey([0xD0; 16]); +const NOT_BILLABLE: MethodKey = MethodKey([0xB0; 16]); +const BILLABLE_HOURS: MethodKey = MethodKey([0xB2; 16]); +const HOURS_BY_BILLABLE: MethodKey = MethodKey([0xB3; 16]); +const READING: ReadingId = ReadingId(1); +const UNIVERSE: Universe = Universe(42); +const SOURCE: &str = "crates/lance-graph-glove-parity/tests/awareness.rs"; +const REV: &str = "worktree"; + +const BILLABLE_TRUE: Cond = Cond::Eq { + field: "billable", + literal: "true", +}; + +const fn query( + filter: &'static [Cond<'static>], + folds: &'static [Fold<'static>], +) -> MethodBody<'static> { + MethodBody::Query { + filter, + folds, + law: Law::Values, + } +} + +fn seal() -> SchemaSeal { + SchemaSeal::of(&canonical_fields()) +} + +fn pure(key: MethodKey, body: MethodBody<'static>) -> MethodSpec<'static> { + MethodSpec { + key, + concept: BILLABLE_WORK_ENTRY, + schema: seal(), + reading: READING, + effect: EffectClass::Pure, + body, + } +} + +/// #1504's methods (`tests/method.rs::specs`), minus the action. +fn specs() -> Vec<(&'static str, MethodSpec<'static>)> { + vec![ + ( + "count_billable", + pure(COUNT_BILLABLE, query(&[BILLABLE_TRUE], &[Fold::Count])), + ), + ( + "count_by_duration", + pure( + COUNT_BY_DURATION, + query( + &[Cond::Eq { + field: "duration", + literal: "1", + }], + &[Fold::Count], + ), + ), + ), + ( + "count_not_billable", + pure( + NOT_BILLABLE, + query( + &[Cond::Ne { + field: "billable", + literal: "true", + }], + &[Fold::Count], + ), + ), + ), + ( + "billable_hours", + pure( + BILLABLE_HOURS, + query(&[BILLABLE_TRUE], &[Fold::Count, Fold::Sum("duration")]), + ), + ), + ( + "hours_by_billable", + pure( + HOURS_BY_BILLABLE, + query( + &[], + &[Fold::GroupSum { + key: "billable", + val: "duration", + groups: 2, + }], + ), + ), + ), + ] +} + +const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; +const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; + +fn indicator(i: usize) -> &'static str { + INDICATORS[(i * 7 + i / 3) % 4] +} + +fn batch(n: usize) -> CatsBatch { + let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") + .lines() + .map(|v| if v == "\\N" { None } else { Some(v) }) + .collect(); + let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); + for i in 0..n { + input[BILLING][i] = Some(indicator(i)); + input[HOURS][i] = Some(HOURS_TEXT[i % 3]); + } + CatsBatch::bind( + CatsSchema::new(42, 0), + std::array::from_fn(|i| input[i].as_slice()), + ) + .unwrap() +} + +/// FIXTURE (as in `tests/method.rs`): SAP's real claim on `duration` is +/// `Hypothesized`. +fn with_duration() -> Vec { + SAP_FIELDS + .iter() + .map(|m| FieldMap { + grade: if m.canonical == "duration" { + Grade::Exact + } else { + m.grade + }, + ..*m + }) + .collect() +} + +fn glove<'a>(basis: &'a [String], binder: &'a dyn Binder, claims: &'a [FieldMap]) -> Glove<'a> { + Glove { + concept: BILLABLE_WORK_ENTRY, + basis, + claims, + binder, + table: TABLE, + reading: READING, + universe: UNIVERSE, + } +} + +struct World { + table: MethodTable, + graph: SoftwareGraph, +} + +fn world(batch: &CatsBatch, claims: &[FieldMap], specs: &[(&str, MethodSpec<'_>)]) -> World { + let basis = canonical_fields(); + let cats = CatsBinder::new(batch); + let table = MethodTable::prepare(&glove(&basis, &cats, claims), specs); + let mut graph = SoftwareGraph::new(); + graph.project(&table, specs, claims, SOURCE, REV); + World { table, graph } +} + +fn programs(t: &MethodTable, key: MethodKey) -> Vec { + t.get(key).unwrap().as_ref().unwrap().programs().to_vec() +} + +fn hours_ident() -> String { + method_ident(BILLABLE_HOURS) +} + +/// FAILS IF: `billable_hours` is not projected with its class, fields, +/// conditions, folds, law, two programs, their primitives, its claims and +/// provenance — each at the tier that knows it. +#[test] +fn billable_hours_is_projected_with_tiers_and_provenance() { + let b = batch(65); + let w = world(&b, &with_duration(), &specs()); + let g = &w.graph; + let m = hours_ident(); + assert_eq!( + g.out(&format!("class:{BILLABLE_WORK_ENTRY:#06x}"), Rel::HasMethod) + .iter() + .filter(|(d, _)| *d == m) + .count(), + 1 + ); + let billable = field_ident(BILLABLE_WORK_ENTRY, seal(), "billable"); + let duration = field_ident(BILLABLE_WORK_ENTRY, seal(), "duration"); + let mut reads: Vec<_> = g.out(&m, Rel::ReadsField); + reads.sort(); + assert_eq!( + reads, + vec![ + (billable.as_str(), Tier::Declared), + (duration.as_str(), Tier::Declared) + ] + ); + assert_eq!( + g.out(&m, Rel::Filters), + vec![(format!("cond:{billable}=true@r1").as_str(), Tier::Declared)] + ); + assert_eq!( + g.out(&m, Rel::FinishedBy), + vec![("law:values", Tier::Declared)] + ); + // Two folds, two programs: the compiler emits the selection once per fold. + let progs = g.out(&m, Rel::LowersTo); + assert_eq!(progs.len(), 2); + assert!(progs.iter().all(|(_, t)| *t == Tier::Compiled)); + // The primitives are read from the programs, not asserted. + let p1 = progs[1].0; + let prims: Vec<&str> = g.out(p1, Rel::UsesPrimitive).iter().map(|x| x.0).collect(); + let real = &programs(&w.table, BILLABLE_HOURS)[1]; + assert!(prims.contains(&"prim:MaskedSumI32")); + assert_eq!( + prims.len(), + { + let mut n: Vec = real + .ops + .iter() + .map(lance_graph_glove_parity::awareness::op_name) + .collect(); + n.push("MaskedSumI32".into()); + n.sort(); + n.dedup(); + n.len() + }, + "one primitive node per distinct op kind of the real program" + ); + // The claim behind `duration` is a derived node; with SAP's real claim + // it is hypothesized. + assert_eq!( + g.out(&duration, Rel::DependsOnClaim), + vec![("claim:hours_logged:exact", Tier::Derived)] + ); + // Provenance names the declaring file, revision, seal and reading. + let e = g + .edges() + .iter() + .find(|e| e.rel == Rel::LowersTo && g.nodes()[e.src as usize].ident == m) + .unwrap(); + let p = g.provenance(e); + assert_eq!((p.source.as_str(), p.rev.as_str()), (SOURCE, REV)); + assert_eq!((p.seal, p.reading), (seal(), READING)); +} + +/// Query A. FAILS IF: the dependency query does not return exactly the +/// methods whose compiled programs contain the primitive, or answers for a +/// primitive no program uses. +#[test] +fn dependency_query_runs_through_quack_and_matches_the_programs() { + let b = batch(65); + let w = world(&b, &with_duration(), &specs()); + // Oracle: the programs themselves. + let using = |terminal: &str| -> Vec { + let mut v: Vec = specs() + .iter() + .filter(|(_, s)| { + w.table.get(s.key).unwrap().as_ref().is_ok_and(|p| { + p.programs().iter().any(|q| { + lance_graph_glove_parity::awareness::terminal_name(&q.terminal) == terminal + }) + }) + }) + .map(|(_, s)| method_ident(s.key)) + .collect(); + v.sort(); + v + }; + for prim in ["MaskedSumI32", "Count", "GroupSumI32"] { + let mut got: Vec = w + .graph + .methods_using(prim) + .unwrap() + .into_iter() + .map(str::to_owned) + .collect(); + got.sort(); + assert_eq!(got, using(prim), "{prim}"); + assert!(!got.is_empty(), "{prim}: non-vacuous"); + } + assert_eq!( + w.graph.methods_using("MaskedSumI32").unwrap(), + vec![hours_ident().as_str()] + ); + // Silence twin: a primitive no program uses is not a node, so it does + // not bind (nothing is minted). + assert!(w.graph.methods_using("CountKeyRunsU32").is_err()); +} + +/// Query B, the claim behind a refusal. FAILS IF: under SAP's real claims +/// `billable_hours` is not recorded as refused, or the field it needs does not +/// lead to the hypothesized claim that blocks it. +#[test] +fn a_hypothesized_claim_refuses_the_method_and_the_graph_says_why() { + let b = batch(65); + let w = world(&b, SAP_FIELDS, &specs()); + let refused = w.graph.refused_for("UnmappedRequired").unwrap(); + assert!(refused.contains(&hours_ident().as_str())); + let duration = field_ident(BILLABLE_WORK_ENTRY, seal(), "duration"); + assert_eq!( + w.graph.out(&duration, Rel::DependsOnClaim), + vec![("claim:hours_logged:hypothesized", Tier::Derived)] + ); + // count_billable reads only `billable` and is not refused. + assert!(!refused.contains(&method_ident(COUNT_BILLABLE).as_str())); + assert!(w.graph.out(&hours_ident(), Rel::LowersTo).is_empty()); +} + +/// Falsifier 1. FAILS IF: two concepts sharing a field label share a field +/// node, or a spec of another concept is not refused by the compiler. +#[test] +fn the_same_field_label_in_two_classes_is_two_fields() { + let b = batch(65); + let mut s = specs(); + s.push(( + "document_billable", + MethodSpec { + concept: DOCUMENT, + ..pure( + MethodKey([0xC0; 16]), + query(&[BILLABLE_TRUE], &[Fold::Count]), + ) + }, + )); + let w = world(&b, &with_duration(), &s); + let a = field_ident(BILLABLE_WORK_ENTRY, seal(), "billable"); + let d = field_ident(DOCUMENT, seal(), "billable"); + assert_ne!(a, d); + assert!(w.graph.node(&a).is_some() && w.graph.node(&d).is_some()); + assert_eq!( + w.graph.refused_for("WrongConcept").unwrap(), + vec![method_ident(MethodKey([0xC0; 16])).as_str()] + ); + // Its condition is not the same node as count_billable's. + let same = w.graph.out(&method_ident(COUNT_BILLABLE), Rel::Filters); + let other = w + .graph + .out(&method_ident(MethodKey([0xC0; 16])), Rel::Filters); + assert_ne!(same, other); +} + +/// Falsifier 2. FAILS IF: a cyclic compute DAG is not recorded as the +/// contract's refusal, or an acyclic one is. +#[test] +fn a_compute_cycle_is_the_contracts_refusal() { + let basis = canonical_fields(); + let cyclic = [ + ComputeEdge { + target: 2, + inputs: &[3], + }, + ComputeEdge { + target: 3, + inputs: &[2], + }, + ]; + let acyclic = [ComputeEdge { + target: 2, + inputs: &[3], + }]; + for (edges, refused) in [(&cyclic[..], true), (&acyclic[..], false)] { + let mut g = SoftwareGraph::new(); + let p = g.note(SOURCE, REV); + g.project_compute(BILLABLE_WORK_ENTRY, seal(), &basis, edges, p); + let got = g.refused_for("ComputeCycle").map(|v| v.len()).unwrap_or(0); + assert_eq!(got == 1, refused, "{edges:?}"); + let projected = g + .edges() + .iter() + .filter(|e| e.rel == Rel::ComputesFrom) + .count(); + assert_eq!( + projected, + edges.iter().map(|e| e.inputs.len()).sum::() + ); + } +} + +/// Falsifier 3. FAILS IF: the same fold under another reading is merged with +/// it, or the compiler does not refuse the foreign reading. +#[test] +fn identical_folds_under_two_readings_stay_apart() { + let b = batch(65); + let mut s = specs(); + s.push(( + "billable_hours_r2", + MethodSpec { + reading: ReadingId(2), + ..pure( + MethodKey([0xC1; 16]), + query(&[BILLABLE_TRUE], &[Fold::Count, Fold::Sum("duration")]), + ) + }, + )); + let w = world(&b, &with_duration(), &s); + let r1 = w.graph.out(&hours_ident(), Rel::Folds); + let r2 = w + .graph + .out(&method_ident(MethodKey([0xC1; 16])), Rel::Folds); + assert_eq!(r1.len(), 2); + assert!(r1.iter().all(|x| !r2.contains(x))); + assert!(w + .graph + .refused_for("ReadingMismatch") + .unwrap() + .contains(&method_ident(MethodKey([0xC1; 16])).as_str())); + // The shared-predicate query does not pair them. + for c in w.graph.shared_predicates() { + assert!(!c.methods.contains(&method_ident(MethodKey([0xC1; 16])))); + } +} + +fn run(t: &MethodTable, b: &CatsBatch, key: MethodKey) -> Answer { + let n = b.len(); + let live = live_words(n); + let lanes = b.lanes(); + let planes = Planes { + n_rows: n, + masks: &[live.as_slice()], + lanes: &lanes, + }; + t.call(key, &planes, UNIVERSE, Frontend::Quack).unwrap() +} + +/// Falsifier 4. FAILS IF: a certificate issued for one body or schema is +/// accepted after either changed. +#[test] +fn a_certificate_goes_stale_when_the_body_or_seal_changes() { + let b = batch(4097); + let w = world(&b, &with_duration(), &specs()); + // The oracle actually runs: row loop over the indicator text. + let LaneRef::I32(hours) = b.lanes()[HOURS] else { + panic!("hours is i32") + }; + let rows = (0..b.len()).filter(|&i| billable_code(indicator(i)) == 1); + let (n, s) = rows.fold((0i64, 0i64), |(n, s), i| (n + 1, s + i64::from(hours[i]))); + assert_eq!( + run(&w.table, &b, BILLABLE_HOURS), + Answer::Values(vec![FoldValue::Scalar(n), FoldValue::Scalar(s)]) + ); + let cert = Certificate { + method: BILLABLE_HOURS, + digest: w.graph.digest(BILLABLE_HOURS).unwrap(), + seal: seal(), + test: "awareness.rs::a_certificate_goes_stale…".into(), + }; + let mut g = w.graph.clone(); + let p = g.note(SOURCE, REV); + g.certify(&cert, p).unwrap(); + assert_eq!(g.out(&hours_ident(), Rel::VerifiedBy).len(), 1); + + // The body changes: the literal is now `false`. + let mut changed = specs(); + changed[3].1.body = query( + &[Cond::Eq { + field: "billable", + literal: "false", + }], + &[Fold::Count, Fold::Sum("duration")], + ); + let w2 = world(&b, &with_duration(), &changed); + let mut g2 = w2.graph; + assert_eq!( + g2.certify(&cert, p), + Err(CertError::StaleBody { + method: BILLABLE_HOURS + }) + ); + // The seal changes, the body text does not. + let mut g3 = w.graph.clone(); + let stale = Certificate { + seal: SchemaSeal(seal().0 ^ 1), + ..cert.clone() + }; + assert_eq!( + g3.certify(&stale, p), + Err(CertError::StaleSeal { + method: BILLABLE_HOURS + }) + ); +} + +/// Falsifier 5. FAILS IF: a bag count and a set count over the same edges +/// are treated as one primitive. Two Quack queries over one filter: the +/// number of traversers (`COUNT(*)`) and the number of distinct targets +/// (`COUNT(DISTINCT dst)`, ordered). +#[test] +fn a_bag_count_and_a_set_count_are_different_primitives() { + let filter = Filter::and([Filter::plane(Mask(0)), Filter::cmp(Col(0), Cmp::EqU32(1))]); + let bag = lower(&Query { + filter: filter.clone(), + agg: Agg::Count, + }) + .unwrap(); + let set = lower(&Query { + filter, + agg: Agg::CountDistinctOrderedU32 { key: Col(1) }, + }) + .unwrap(); + let mut g = SoftwareGraph::new(); + let p = g.note(SOURCE, REV); + let bag_m = g.add_method("method:bag"); + g.add_program(bag_m, "program:bag", &bag, p); + let set_m = g.add_method("method:set"); + g.add_program(set_m, "program:set", &set, p); + let bag_t: Vec<_> = g.out("program:bag", Rel::UsesPrimitive); + let set_t: Vec<_> = g.out("program:set", Rel::UsesPrimitive); + assert_ne!(bag_t, set_t); + assert_eq!(g.methods_using("Count").unwrap(), vec!["method:bag"]); + // The ops before the terminal are identical: a label-level reading + // ("both count over the same filter") would merge them. + assert_eq!(bag.ops, set.ops); +} + +/// H1 and falsifier 6. FAILS IF: `billable_hours`'s sum program is not a +/// fusion candidate, or a candidate whose program ANDs a validity plane +/// claims its NULL obligation discharged. +#[test] +fn the_fusion_candidate_keeps_its_null_obligation_open_when_a_validity_plane_gates() { + let b = batch(65); + let w = world(&b, &with_duration(), &specs()); + let progs = programs(&w.table, BILLABLE_HOURS); + let id = |i| format!("program:{}#{i}", "b2".repeat(16)); + let inputs: Vec<(String, &Program)> = vec![(id(0), &progs[0]), (id(1), &progs[1])]; + let c = w.graph.fusion_candidates(&inputs); + assert_eq!(c.len(), 1, "only the MaskedSumI32 program"); + assert_eq!( + c[0].kind, + CandidateKind::FusePredicateIntoSum { program: id(1) } + ); + assert_eq!(c[0].methods, vec![hours_ident()]); + let null = |c: &lance_graph_glove_parity::awareness::Candidate| { + c.obligations + .iter() + .find(|o| o.what.starts_with("NULL")) + .unwrap() + .discharged + .clone() + }; + assert!(null(&c[0]).is_some()); + assert!(c[0].obligations.iter().any(|o| o.discharged.is_none())); + // The same sum, bound three-valued over a nullable lane (validity plane 1). + let nullable = Query { + filter: Filter::and([Filter::plane(Mask(0)), Filter::cmp(Col(0), Cmp::EqU32(1))]) + .sql_where(&[(Col(0), Mask(1))]), + agg: Agg::SumI32(Col(2)), + }; + let p = lower(&nullable).unwrap(); + let c = w + .graph + .fusion_candidates(&[("program:nullable".into(), &p)]); + assert_eq!(c.len(), 1); + assert_eq!(null(&c[0]), None); +} + +/// Falsifier 7. FAILS IF: a transformation faster on one workload and slower +/// on another is reported as an improvement. FIXTURE numbers: they test the +/// rule, they are not measurements. +#[test] +fn a_mixed_result_is_not_an_improvement() { + let faster = Workload { + before_ns: 600.0, + after_ns: 366.0, + }; + let slower = Workload { + before_ns: 56.0, + after_ns: 70.0, + }; + assert_eq!(verdict(&[faster, slower]), Verdict::Mixed); + assert_eq!(verdict(&[faster, faster]), Verdict::Improves); + assert_eq!(verdict(&[slower]), Verdict::Regresses); + let tie = Workload { + before_ns: 1.0, + after_ns: 1.0, + }; + assert_eq!(verdict(&[faster, tie]), Verdict::Mixed); + assert_eq!(verdict(&[]), Verdict::Unmeasured); +} + +/// Falsifier 8. FAILS IF: a primitive claimed for a method that none of its +/// compiled programs contains goes unreported, or a true claim is reported. +#[test] +fn a_claimed_primitive_must_be_in_the_lowered_program() { + let b = batch(65); + let mut w = world(&b, &with_duration(), &specs()); + let p = w.graph.note(SOURCE, REV); + let m = hours_ident(); + w.graph.claim( + &m, + Rel::UsesPrimitive, + "prim:MaskedSumI32", + Tier::Declared, + p, + ); + assert!(w.graph.primitive_claim_mismatches().is_empty()); + w.graph.claim( + &m, + Rel::UsesPrimitive, + "prim:GroupSumI32", + Tier::Declared, + p, + ); + let bad = w.graph.primitive_claim_mismatches(); + assert_eq!(bad.len(), 1); + assert_eq!( + (bad[0].method.as_str(), bad[0].primitive.as_str()), + (m.as_str(), "prim:GroupSumI32") + ); +} + +/// FIXTURE: the NARS evidence glove of `tests/polyfill.rs`, reduced to what +/// preparation reads. +const NARS: u16 = 0xFFF0; +const EVIDENCE: &str = "evidence"; + +struct EvidenceBinder; + +impl Binder for EvidenceBinder { + fn table(&self, name: &str) -> Option { + (name == EVIDENCE).then_some(TableId(0)) + } + fn live(&self, _: TableId) -> Mask { + Mask(0) + } + fn field(&self, _: TableId, name: &str) -> Option { + let (col, kind) = match name { + "statement" => (0, FieldKind::Code), + "stamp" => (1, FieldKind::Code), + "independent" => (2, FieldKind::Code), + "w" => (3, FieldKind::I32), + "wf" => (4, FieldKind::I32), + _ => return None, + }; + Some(BoundField { + col: Col(col), + kind, + validity: None, + }) + } + fn code(&self, _: TableId, col: Col, literal: &str) -> Option { + (col.0 == 2) + .then_some(match literal { + "true" => Some(1), + "false" => Some(0), + _ => None, + }) + .flatten() + } +} + +fn nars_table( + filter: &'static [Cond<'static>], +) -> (MethodTable, Vec<(&'static str, MethodSpec<'static>)>) { + let basis: Vec = ["statement", "stamp", "independent", "w", "wf"] + .map(String::from) + .to_vec(); + let claims: Vec = ["statement", "stamp", "independent", "w", "wf"] + .map(|f| FieldMap { + native: f, + canonical: f, + grade: Grade::Exact, + note: "fixture", + }) + .to_vec(); + const FOLDS: &[Fold] = &[ + Fold::GroupSum { + key: "statement", + val: "w", + groups: 4, + }, + Fold::GroupSum { + key: "statement", + val: "wf", + groups: 4, + }, + ]; + let specs = vec![( + "revision", + MethodSpec { + key: MethodKey([0xEE; 16]), + concept: NARS, + schema: SchemaSeal::of(&basis), + reading: READING, + effect: EffectClass::Pure, + body: MethodBody::Query { + filter, + folds: FOLDS, + law: Law::NarsRevision { + independent: "independent", + scale: 1000, + horizon: 1, + }, + }, + }, + )]; + let t = MethodTable::prepare( + &Glove { + concept: NARS, + basis: &basis, + claims: &claims, + binder: &EvidenceBinder, + table: EVIDENCE, + reading: READING, + universe: UNIVERSE, + }, + &specs, + ); + (t, specs) +} + +/// Falsifier 9 and query C. FAILS IF: a capability is shipped because a +/// method declares it, a gap is "demonstrated" without a compiler refusal, +/// or a primitive a program uses is not shipped. +#[test] +fn a_declared_need_never_ships_a_capability() { + let b = batch(65); + let mut w = world(&b, &with_duration(), &specs()); + let p = w.graph.note( + ".claude/board/entries/2026-10-12-class-method-polyfill-ledger.md", + REV, + ); + let g = &mut w.graph; + assert_eq!(g.capability("prim:MaskedSumI32"), CapStatus::Shipped); + // Hypotheses from other domains: recorded, never promoted. + g.claim( + "method:iam_reconcile", + Rel::MayRequire, + "cap:FirstOfRun", + Tier::Hypothesis, + p, + ); + g.claim( + "method:sap_import", + Rel::MayRequire, + "cap:FirstOfRun", + Tier::Hypothesis, + p, + ); + // Even a method that says it USES it does not make it shipped. + g.claim( + &hours_ident(), + Rel::UsesPrimitive, + "cap:FirstOfRun", + Tier::Declared, + p, + ); + assert!(matches!(g.capability("cap:FirstOfRun"), CapStatus::Claimed { by } if by.len() == 3)); + // A gap cannot be demonstrated by a refusal nobody compiled. + assert_eq!( + g.blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", p), + Err("refusal:EvidenceNotIndependent".into()) + ); + // Revision without the payload's independence mark: the compiler refuses + // it. FirstOfRun is what would compute that mark. + let (t, s) = nars_table(&[]); + g.project(&t, &s, &[], SOURCE, REV); + g.blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", p) + .unwrap(); + assert_eq!( + g.capability("cap:FirstOfRun"), + CapStatus::Demonstrated { + refusals: vec!["refusal:EvidenceNotIndependent".into()] + } + ); + // Silence twin: with the mark in the filter, revision prepares and no + // such refusal exists to demonstrate anything. + const INDEP: &[Cond] = &[Cond::Eq { + field: "independent", + literal: "true", + }]; + let (t, s) = nars_table(INDEP); + let mut clean = SoftwareGraph::new(); + clean.project(&t, &s, &[], SOURCE, REV); + let q = clean.note(SOURCE, REV); + assert!(clean + .blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", q) + .is_err()); + assert_eq!( + clean.methods_using("GroupSumI32").unwrap(), + vec![method_ident(MethodKey([0xEE; 16])).as_str()] + ); +} + +/// Falsifier 10. FAILS IF: a cost measured on one CPU tier is reported for +/// another. +#[test] +fn a_measurement_answers_only_for_its_own_tier() { + let mut g = SoftwareGraph::new(); + let v3 = CpuTier { + target: "x86-64-v3".into(), + avx512f: true, + }; + let v4 = CpuTier { + target: "x86-64-v4".into(), + avx512f: true, + }; + let prog = format!("program:{}#1", "b2".repeat(16)); + // The #1504 ledger's recorded median (`method_cost.rs`, 1,048,576 rows). + g.measure(Measurement { + program: prog.clone(), + rows: 1 << 20, + tier: v3.clone(), + median_ns: 600_000.0, + }); + assert_eq!(g.cost(&prog, 1 << 20, &v3), Ok(600_000.0)); + assert_eq!( + g.cost(&prog, 1 << 20, &v4), + Err(CostError::TierMismatch { measured: v3 }) + ); + assert_eq!(g.cost(&prog, 1 << 16, &v4), Err(CostError::Unmeasured)); +} + +/// Query D. FAILS IF: the shared condition is not found across the methods +/// that carry it, or a method's per-fold rescans are not reported. +#[test] +fn repeated_structure_is_found_by_identity() { + let b = batch(65); + let w = world(&b, &with_duration(), &specs()); + let shared = w.graph.shared_predicates(); + let billable = field_ident(BILLABLE_WORK_ENTRY, seal(), "billable"); + let want = Candidate { + kind: CandidateKind::SharedPredicate { + conds: vec![format!("cond:{billable}=true@r1")], + }, + methods: vec![method_ident(COUNT_BILLABLE), hours_ident()], + obligations: shared[0].obligations.clone(), + }; + assert_eq!(shared, vec![want]); + // `!= true` is another condition, not a repeat of `= true`. + assert!(!shared[0].methods.contains(&method_ident(NOT_BILLABLE))); + // billable_hours scans its selection once per fold. + let sel = w.graph.shared_selections(&w.table); + assert_eq!(sel.len(), 1); + assert_eq!(sel[0].methods, vec![hours_ident()]); + assert!(sel[0].obligations.iter().all(|o| o.discharged.is_none())); +} + +/// The MDL experiment (plan §5). FAILS IF: factoring a condition set shared +/// by two methods does not shorten the description when the set has two +/// conditions, shortens it when it has one, or a label-only reading counts +/// a cross-class repeat that typed identities do not. +#[test] +fn description_length_rewards_only_typed_multi_condition_reuse() { + let s = |v: &[&str]| v.iter().map(|x| (*x).to_owned()).collect::>(); + // One condition, shared by two methods (count_billable, billable_hours). + let one = [ + s(&["c:billable=true", "f:count", "l:values"]), + s(&["c:billable=true", "f:count", "f:sum", "l:values"]), + ]; + let base = description_length(&one, &[]); + let factored = description_length(&one, &[s(&["c:billable=true"])]); + assert!(factored.total() >= base.total(), "{factored:?} vs {base:?}"); + // Two conditions, shared by three methods. + let two = [ + s(&[ + "c:billable=true", + "c:status=approved", + "f:count", + "l:values", + ]), + s(&["c:billable=true", "c:status=approved", "f:sum", "l:values"]), + s(&[ + "c:billable=true", + "c:status=approved", + "f:group_sum", + "l:values", + ]), + ]; + let base = description_length(&two, &[]); + let factored = description_length(&two, &[s(&["c:billable=true", "c:status=approved"])]); + assert!(factored.total() < base.total(), "{factored:?} vs {base:?}"); + // Counterexample: the same LABELS in two classes. Read by label, they + // repeat and factoring "saves" bits; read by typed identity, they do not + // repeat and factoring saves nothing. + // The case above, read twice: by label (it repeats) and by identity, + // where each method's conditions belong to another class (no repeat). + let by_label = two.clone(); + let by_identity = [ + s(&[ + "c:0x0103/billable=true", + "c:0x0103/status=approved", + "f:count", + "l:values", + ]), + s(&[ + "c:0x0201/billable=true", + "c:0x0201/status=approved", + "f:sum", + "l:values", + ]), + s(&[ + "c:0x0202/billable=true", + "c:0x0202/status=approved", + "f:group_sum", + "l:values", + ]), + ]; + let label_gain = description_length(&by_label, &[]).total() + - description_length(&by_label, &[s(&["c:billable=true", "c:status=approved"])]).total(); + let typed_gain = description_length(&by_identity, &[]).total() + - description_length( + &by_identity, + &[s(&["c:0x0103/billable=true", "c:0x0103/status=approved"])], + ) + .total(); + assert!(label_gain > 0.0, "{label_gain}"); + assert!(typed_gain <= 0.0, "{typed_gain}"); +} + +/// The graph's own bodies feed the MDL measure by identity. FAILS IF the +/// declared tokens of a projected method are not its condition, fold and law +/// identities. +#[test] +fn the_graph_supplies_typed_bodies() { + let b = batch(65); + let w = world(&b, &with_duration(), &specs()); + let bodies = w.graph.bodies(); + let (_, hours) = bodies.iter().find(|(m, _)| *m == hours_ident()).unwrap(); + assert_eq!(hours.len(), 4, "{hours:?}"); + assert!(hours.iter().any(|t| t.starts_with("cond:field:0x0103/"))); + assert!(hours.contains(&"law:values".to_owned())); +} From eaa58f6f47c0f88f287240513c26236a0f541f80 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:10:41 +0000 Subject: [PATCH 2/7] glove-parity: one identity, one kind in the awareness graph A disable run showed blocked_by's compiled-refusal guard was never exercised. The case that exercises it (a refusal noted by hand, then the compiler's) found a second defect: claim() picked the target's kind from a narrow prefix check, so a noted refusal became a Capability node and the compiler's later refusal reused it. A node's kind now comes from its identity prefix (kind_of), interning asserts it, and an identity with no known prefix is refused. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .../lance-graph-glove-parity/src/awareness.rs | 60 ++++++++++++++--- .../tests/awareness.rs | 67 ++++++++++++++----- 2 files changed, 99 insertions(+), 28 deletions(-) diff --git a/crates/lance-graph-glove-parity/src/awareness.rs b/crates/lance-graph-glove-parity/src/awareness.rs index 8a3484e41..d3be6f216 100644 --- a/crates/lance-graph-glove-parity/src/awareness.rs +++ b/crates/lance-graph-glove-parity/src/awareness.rs @@ -301,6 +301,30 @@ fn hex(bytes: &[u8]) -> String { }) } +/// A node's kind, from its identity's prefix: one identity, one kind. +#[must_use] +pub fn kind_of(ident: &str) -> Option { + const PREFIXES: [(&str, Kind); 13] = [ + ("class:", Kind::Class), + ("method:", Kind::Method), + ("field:", Kind::Field), + ("cond:", Kind::Cond), + ("fold:", Kind::Fold), + ("law:", Kind::Law), + ("program:", Kind::Program), + ("prim:", Kind::Primitive), + ("lowering:", Kind::Lowering), + ("refusal:", Kind::Refusal), + ("claim:", Kind::Claim), + ("cap:", Kind::Capability), + ("test:", Kind::Test), + ]; + PREFIXES + .iter() + .find(|(p, _)| ident.starts_with(p)) + .map(|(_, k)| *k) +} + /// The identity of a method node. #[must_use] pub fn method_ident(key: MethodKey) -> String { @@ -479,6 +503,7 @@ impl SoftwareGraph { } fn intern(&mut self, kind: Kind, ident: String) -> u32 { + debug_assert_eq!(kind_of(&ident), Some(kind), "{ident}"); if let Some(i) = self.node(&ident) { return i; } @@ -655,17 +680,32 @@ impl SoftwareGraph { (self.provs.len() - 1) as u32 } - /// Record a need, or a claim, by identity: `method REL capability` at - /// `tier`. Recording never ships anything (see [`Self::capability`]). - pub fn claim(&mut self, method: &str, rel: Rel, target: &str, tier: Tier, prov: u32) { - let kind = if target.starts_with("prim:") { - Kind::Primitive - } else { - Kind::Capability - }; - let m = self.intern(Kind::Method, method.to_owned()); - let c = self.intern(kind, target.to_owned()); + /// Record a need, or a claim, by identity: `src REL target` at `tier`. + /// Each node's kind is read from its identity's prefix, so a noted + /// `refusal:` is the same refusal node the compiler writes, never a + /// second node of another kind. Recording never ships anything (see + /// [`Self::capability`]). + /// + /// # Errors + /// + /// The identity whose prefix names no kind. + pub fn claim( + &mut self, + src: &str, + rel: Rel, + target: &str, + tier: Tier, + prov: u32, + ) -> Result<(), String> { + let m = self.intern_ident(src)?; + let c = self.intern_ident(target)?; self.link(m, rel, c, tier, m, prov); + Ok(()) + } + + fn intern_ident(&mut self, ident: &str) -> Result { + let kind = kind_of(ident).ok_or_else(|| ident.to_owned())?; + Ok(self.intern(kind, ident.to_owned())) } /// A method node by identity, for programs not prepared by a table. diff --git a/crates/lance-graph-glove-parity/tests/awareness.rs b/crates/lance-graph-glove-parity/tests/awareness.rs index 37ba13b87..3897af0f4 100644 --- a/crates/lance-graph-glove-parity/tests/awareness.rs +++ b/crates/lance-graph-glove-parity/tests/awareness.rs @@ -613,21 +613,25 @@ fn a_claimed_primitive_must_be_in_the_lowered_program() { let mut w = world(&b, &with_duration(), &specs()); let p = w.graph.note(SOURCE, REV); let m = hours_ident(); - w.graph.claim( - &m, - Rel::UsesPrimitive, - "prim:MaskedSumI32", - Tier::Declared, - p, - ); + w.graph + .claim( + &m, + Rel::UsesPrimitive, + "prim:MaskedSumI32", + Tier::Declared, + p, + ) + .unwrap(); assert!(w.graph.primitive_claim_mismatches().is_empty()); - w.graph.claim( - &m, - Rel::UsesPrimitive, - "prim:GroupSumI32", - Tier::Declared, - p, - ); + w.graph + .claim( + &m, + Rel::UsesPrimitive, + "prim:GroupSumI32", + Tier::Declared, + p, + ) + .unwrap(); let bad = w.graph.primitive_claim_mismatches(); assert_eq!(bad.len(), 1); assert_eq!( @@ -756,14 +760,16 @@ fn a_declared_need_never_ships_a_capability() { "cap:FirstOfRun", Tier::Hypothesis, p, - ); + ) + .unwrap(); g.claim( "method:sap_import", Rel::MayRequire, "cap:FirstOfRun", Tier::Hypothesis, p, - ); + ) + .unwrap(); // Even a method that says it USES it does not make it shipped. g.claim( &hours_ident(), @@ -771,9 +777,34 @@ fn a_declared_need_never_ships_a_capability() { "cap:FirstOfRun", Tier::Declared, p, - ); + ) + .unwrap(); assert!(matches!(g.capability("cap:FirstOfRun"), CapStatus::Claimed { by } if by.len() == 3)); - // A gap cannot be demonstrated by a refusal nobody compiled. + // An identity whose prefix names no kind is refused, not guessed. + assert_eq!( + g.claim( + "method:x", + Rel::MayRequire, + "FirstOfRun", + Tier::Hypothesis, + p + ), + Err("FirstOfRun".into()) + ); + // A gap cannot be demonstrated by a refusal nobody compiled: not by an + // absent one, and not by one a person only noted. + assert_eq!( + g.blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", p), + Err("refusal:EvidenceNotIndependent".into()) + ); + g.claim( + "method:a_note", + Rel::RefusedFor, + "refusal:EvidenceNotIndependent", + Tier::Hypothesis, + p, + ) + .unwrap(); assert_eq!( g.blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", p), Err("refusal:EvidenceNotIndependent".into()) From b007bb8b4c2f41141ca0db290f9af1f8193615e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:12:52 +0000 Subject: [PATCH 3/7] =?UTF-8?q?glove-parity:=20awareness=5Floop=20example?= =?UTF-8?q?=20=E2=80=94=20graph=20=E2=86=92=20H1=20candidate=20=E2=86=92?= =?UTF-8?q?=20tier-labelled=20timing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Projects two prepared methods, asks the graph (through Quack) which programs use MaskedSumI32 and which are fusion candidates, times each candidate against a fused scalar reference after asserting both give the same answer, and records the timings as Measurements on the compiled and detected tier. A cost asked for another tier is refused (TierMismatch). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .../examples/awareness_loop.rs | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 crates/lance-graph-glove-parity/examples/awareness_loop.rs diff --git a/crates/lance-graph-glove-parity/examples/awareness_loop.rs b/crates/lance-graph-glove-parity/examples/awareness_loop.rs new file mode 100644 index 000000000..206e5f23d --- /dev/null +++ b/crates/lance-graph-glove-parity/examples/awareness_loop.rs @@ -0,0 +1,236 @@ +//! D-SAG-1, the loop run once: project prepared methods into the +//! software-awareness graph, ask it which programs are H1 fusion candidates, +//! time each candidate against a fused scalar reference on THIS machine, and +//! record the timings as tier-labelled evidence. +//! +//! ```text +//! cargo run --release --example awareness_loop [rows] [reps] +//! ``` +//! +//! The scalar loop is a reference for what one pass costs, not a fused +//! mask-risc terminal: no such terminal exists (`FusedFold` is `Count`/`Any`), +//! so the graph records the candidate's obligations open and the gap as a +//! measured ratio, never as an achieved speed-up. +use std::hint::black_box; +use std::time::Instant; + +use lance_graph_glove_parity::awareness::{CpuTier, Measurement, SoftwareGraph}; +use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; +use lance_graph_glove_parity::method::{ + Cond, EffectClass, Fold, Glove, Law, MethodBody, MethodKey, MethodSpec, MethodTable, ReadingId, + SchemaSeal, Universe, +}; +use lance_graph_mask_risc::{execute_into, Foreign, LaneRef, Out, Planes, Program, Scratch, Value}; +use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; +use lance_graph_sap::bind::{CatsBatch, BILLABLE, BILLING, HOURS}; +use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; +use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; + +const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; +const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; +const BILLABLE_TRUE: Cond = Cond::Eq { + field: "billable", + literal: "true", +}; + +fn batch(n: usize) -> CatsBatch { + let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") + .lines() + .map(|v| if v == "\\N" { None } else { Some(v) }) + .collect(); + let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); + for i in 0..n { + input[BILLING][i] = Some(INDICATORS[(i * 7 + i / 3) % 4]); + input[HOURS][i] = Some(HOURS_TEXT[i % 3]); + } + CatsBatch::bind( + CatsSchema::new(42, 0), + std::array::from_fn(|i| input[i].as_slice()), + ) + .unwrap() +} + +fn tier() -> CpuTier { + CpuTier { + target: format!( + "compiled avx512f={} avx2={}", + cfg!(target_feature = "avx512f"), + cfg!(target_feature = "avx2") + ), + #[cfg(target_arch = "x86_64")] + avx512f: std::is_x86_feature_detected!("avx512f"), + #[cfg(not(target_arch = "x86_64"))] + avx512f: false, + } +} + +fn main() { + let mut args = std::env::args().skip(1); + let n: usize = args.next().map_or(1 << 20, |s| s.parse().unwrap()); + let reps: usize = args.next().map_or(51, |s| s.parse().unwrap()); + let rev = std::env::var("SAG_REV").unwrap_or_else(|_| "worktree".into()); + + let batch = batch(n); + let cats = CatsBinder::new(&batch); + let basis = canonical_fields(); + // FIXTURE (as in `method_cost.rs`): SAP's real claim on `duration` is + // `Hypothesized`; under it `billable_hours` is refused, and the graph + // records that refusal instead of a program. + let claims: Vec = SAP_FIELDS + .iter() + .map(|m| FieldMap { + grade: if m.canonical == "duration" { + Grade::Exact + } else { + m.grade + }, + ..*m + }) + .collect(); + let spec = |key, folds: &'static [Fold<'static>]| MethodSpec { + key: MethodKey(key), + concept: BILLABLE_WORK_ENTRY, + schema: SchemaSeal::of(&basis), + reading: ReadingId(1), + effect: EffectClass::Pure, + body: MethodBody::Query { + filter: &[BILLABLE_TRUE], + folds, + law: Law::Values, + }, + }; + let specs = [ + ("count_billable", spec([0xB1; 16], &[Fold::Count])), + ("billable_hours", spec([0xB2; 16], &[Fold::Sum("duration")])), + ]; + let glove = Glove { + concept: BILLABLE_WORK_ENTRY, + basis: &basis, + claims: &claims, + binder: &cats, + table: TABLE, + reading: ReadingId(1), + universe: Universe(42), + }; + let table = MethodTable::prepare(&glove, &specs); + let mut g = SoftwareGraph::new(); + g.project( + &table, + &specs, + &claims, + "crates/lance-graph-glove-parity/examples/awareness_loop.rs", + &rev, + ); + println!( + "graph: {} nodes, {} edges, rev {rev}", + g.nodes().len(), + g.edges().len() + ); + println!( + "methods using MaskedSumI32 (Quack over the edge table): {:?}", + g.methods_using("MaskedSumI32").unwrap() + ); + + let mut programs: Vec<(String, Program)> = Vec::new(); + for (_, s) in &specs { + if let Some(Ok(p)) = table.get(s.key) { + for (i, q) in p.programs().iter().enumerate() { + let hex: String = s.key.0.iter().map(|b| format!("{b:02x}")).collect(); + programs.push((format!("program:{hex}#{i}"), q.clone())); + } + } + } + let refs: Vec<(String, &Program)> = programs.iter().map(|(i, p)| (i.clone(), p)).collect(); + let candidates = g.fusion_candidates(&refs); + println!("H1 candidates: {}", candidates.len()); + + let live = live_words(n); + let lanes = batch.lanes(); + let planes = Planes { + n_rows: n, + masks: &[live.as_slice()], + lanes: &lanes, + }; + let (LaneRef::U32(bill), LaneRef::I32(hours)) = (lanes[BILLABLE], lanes[HOURS]) else { + panic!("lane kinds") + }; + let scalar = || -> i64 { + bill.iter() + .zip(hours) + .filter(|(b, _)| **b == 1) + .map(|(_, h)| i64::from(*h)) + .sum() + }; + let t = tier(); + for c in &candidates { + let lance_graph_glove_parity::awareness::CandidateKind::FusePredicateIntoSum { program } = + &c.kind + else { + continue; + }; + let p = &programs.iter().find(|(i, _)| i == program).unwrap().1; + let mut scratch = Scratch::for_program(p, n).unwrap(); + let mut run = + || match execute_into(p, &planes, &Foreign::NONE, &mut scratch, Out::None).unwrap() { + Value::SumI64(s) => s, + v => panic!("{v:?}"), + }; + // Both paths must give the same answer before either is timed. + assert_eq!(run(), scalar(), "the reference computes another answer"); + let (mut a, mut b) = (Vec::new(), Vec::new()); + for r in 0..reps { + for k in 0..2 { + let t0 = Instant::now(); + if (r + k) % 2 == 0 { + black_box(run()); + a.push(t0.elapsed().as_nanos() as f64); + } else { + black_box(scalar()); + b.push(t0.elapsed().as_nanos() as f64); + } + } + } + a.sort_by(f64::total_cmp); + b.sort_by(f64::total_cmp); + let (ma, mb) = (a[a.len() / 2], b[b.len() / 2]); + g.measure(Measurement { + program: program.clone(), + rows: n, + tier: t.clone(), + median_ns: ma, + }); + g.measure(Measurement { + program: format!("reference:scalar_fused:{program}"), + rows: n, + tier: t.clone(), + median_ns: mb, + }); + println!("candidate {program} of {:?}", c.methods); + for o in &c.obligations { + println!( + " obligation: {} — {}", + o.what, + o.discharged.as_deref().unwrap_or("OPEN") + ); + } + println!( + " tier {:?}; rows {n}; reps {reps}; median program {:.1} µs, scalar reference {:.1} µs, ratio {:.2}", + t, + ma / 1e3, + mb / 1e3, + ma / mb + ); + println!(" recorded cost on this tier: {:?}", g.cost(program, n, &t)); + let other = CpuTier { + target: "x86-64-v3".into(), + avx512f: t.avx512f, + }; + if other != t { + println!( + " asked for another tier ({}): {:?}", + other.target, + g.cost(program, n, &other) + ); + } + } +} From 2cca71130dea490fff5d00fb2e99c323ff5634cb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:15:03 +0000 Subject: [PATCH 4/7] board: D-SAG-1 software-awareness graph plan, status rows, entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/INTEGRATION_PLANS.md | 9 + .claude/board/STATUS_BOARD.md | 14 + .../2026-10-12-software-awareness-graph.md | 17 + .claude/board/entries/README.md | 3 +- .claude/plans/software-awareness-graph-v1.md | 383 ++++++++++++++++++ 5 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 .claude/board/entries/2026-10-12-software-awareness-graph.md create mode 100644 .claude/plans/software-awareness-graph-v1.md diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index bde53bfba..a55488535 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,12 @@ +## 2026-10-12 (4) — software-awareness-graph-v1 — the software's own methods, programs and evidence as graph rows → `.claude/plans/software-awareness-graph-v1.md` + +**Status:** MEASURED for one concept's methods (D-SAG-1-0..3 in the PR); D-SAG-1-4..6 queued. + +- A `MethodTable` projects to typed rows (class, method, field, condition, fold, law, program, primitive, lowering, refusal, claim, capability, test). Each row has a tier (declared / derived / compiled / verified / hypothesis) and a provenance. +- The edge table is a Quack table: dependency questions are Drafts lowered and run on mask-risc. Validation is the compiler's verdicts recorded as edges. +- H1 (fuse a predicate into `MaskedSumI32`) comes from the rows. Measured gap: 1.6–1.8× against a single-pass scalar reference on two tiers. Missing: `Pred` fusion and a sum `FusedFold`. +- First blocker: multi-hop questions need a relation carrier, not a mask fed between programs. + ## 2026-10-12 (2) — nal-polyfill-v1 — `NAL::narsese()` as a polyfill over R2IL procedures and mask-risc → `.claude/plans/nal-polyfill-v1.md` **Status:** PROPOSAL (D-NALP-0..11). Only D-NALP-0 (this plan); each wave is gated on the one before. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 57b64ae80..d4d4d400d 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -1,3 +1,17 @@ +## D-SAG-1 — Software-awareness graph (2026-10-12) + +Plan: `.claude/plans/software-awareness-graph-v1.md`. No new opcode, Quack variant, engine or registry. Nothing applies a transformation. + +| D-id | scope | status | gate / falsifier | +|---|---|---|---| +| D-SAG-1-0 | inventory: Bible work (6-lane fixture, English KJV; no 7-language graph in code), OGAR identities, dir-sim provenance, Quack/mask-risc | In PR | every row cites code or says documented-only | +| D-SAG-1-1 | typed schema + projection of prepared methods with tiers and provenance | In PR | `tests/awareness.rs` projection test; one identity, one kind | +| D-SAG-1-2 | queries A–D through Quack; compiler verdicts as edges | In PR | answers == the programs; falsifiers 1–10, each disable-verified | +| D-SAG-1-3 | H1 candidate + tier-labelled measurement; MDL measure with counterexample | In PR | `examples/awareness_loop.rs`; MDL test | +| D-SAG-1-4 | multi-hop dependency via relation carrier / factored `EqU32Via` | Queued (needs D-CMH-G1-4) | two-hop answer == closure computed from the programs | +| D-SAG-1-5 | G1 `CALLS` edges + method-reference refusals | Queued (after #1508) | `MethodCycle` etc. recorded, never re-derived | +| D-SAG-1-6 | persist as a Lance dataset; source revision from git; canonical body digest | Queued | reload equals projection; toolchain change keeps digests | + ## D-NALP — `NAL::narsese()` polyfill over R2IL procedures and mask-risc (2026-10-12) Plan: `.claude/plans/nal-polyfill-v1.md`. Template: D-SQLP. No opcode, tenant or ABI change is authorized. diff --git a/.claude/board/entries/2026-10-12-software-awareness-graph.md b/.claude/board/entries/2026-10-12-software-awareness-graph.md new file mode 100644 index 000000000..52503ac54 --- /dev/null +++ b/.claude/board/entries/2026-10-12-software-awareness-graph.md @@ -0,0 +1,17 @@ +# D-SAG-1 — the software's own methods as graph rows (2026-10-12) + +**Status:** MEASURED (one concept, one hop). Plan: `.claude/plans/software-awareness-graph-v1.md`. + +- **Projection.** `crates/lance-graph-glove-parity/src/awareness.rs` projects a `MethodTable` into typed node and edge rows. Every edge carries a tier and a provenance. The edge table is a Quack table, queried by `Draft` → `lower` → mask-risc, landing on the owning method in one program. +- **Evidence chain.** Under SAP's real claims the graph explains `billable_hours`' refusal down to `claim:hours_logged:hypothesized`. `FirstOfRun` counts as demonstrated only once the compiler's `EvidenceNotIndependent` refusal is projected. IAM and SAP needs stay hypotheses. +- **H1.** The tiled `MaskedSumI32` program measures 1.6–1.8× a single-pass scalar reference: + - native: 603.5/379.1 µs and 619.6/362.8 µs; + - config-v3: 617.4/347.6 µs; + - 1,048,576 rows. + + A fused mask-risc terminal is absent; `fused_ternlog` declines `Pred` and `FusedFold` is `Count`/`Any` only. +- **MDL.** Factoring one shared condition never shortens the description, so G1's `is_billable` is a semantic reuse, not a syntactic one. The first metric dropped a composite's atoms from the vocabulary, and the label-vs-identity counterexample caught it. +- **Disable runs.** 14 runs, all red. One disable first stayed green; the missing test case then exposed a kind-confusion defect in `claim` (fixed). +- **Blocker.** Multi-hop questions need a relation carrier or a factored `EqU32Via`, not a mask passed between programs. + +OPEN: method identity is caller-issued (OGAR mints none); the body digest is build-local. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 028a3912a..edd308029 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ 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. -315 entries, 2026-08-06 .. 2026-10-12. +316 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 | `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) | | 2026-10-12 | `hyperon-metta-lg-translation` | Hyperon/MeTTa to LG sibling map: atom/hypergraph roundtrip, binding and rewrite parity, typed folds, scoped effects and 15 open questions | [2026-10-12-hyperon-metta-lg-translation.md](2026-10-12-hyperon-metta-lg-translation.md) | diff --git a/.claude/plans/software-awareness-graph-v1.md b/.claude/plans/software-awareness-graph-v1.md new file mode 100644 index 000000000..d0f3ede83 --- /dev/null +++ b/.claude/plans/software-awareness-graph-v1.md @@ -0,0 +1,383 @@ +# D-SAG-1 — Software-awareness graph: the methods, programs and evidence of lance-graph as graph rows + +**Status:** MEASURED for one concept's methods (#1504's `BillableWorkEntry` set). +Code: `crates/lance-graph-glove-parity/src/awareness.rs`, tests +`tests/awareness.rs` (16), example `examples/awareness_loop.rs`. + +**Research question.** Can lance-graph represent its own methods, dependencies, +semantic contracts and execution evidence as graph rows in its existing +substrate, and then use that representation to find and check structural +optimizations? + +**Answer so far.** Yes, for one concept and one hop. +- The rows are produced by projecting a `MethodTable`. +- They are queried with an ordinary Quack `Draft`, lowered and run on mask-risc. +- The graph found one optimization candidate (H1). That candidate has a + measured cost gap and an open list of obligations. +- The first blocker sits exactly where a question needs more than one hop + (§9). + +**SoC** in this repository means *separation of concerns* (about 55 of 88 +occurrences; "system on chip" occurs 0 times). "SoC self-optimization" is read +here as: detect where concerns are duplicated or tangled across methods, and +propose a factoring. Nothing in this plan concerns hardware. + +## Checklist + +- [x] 1. Inventory of reusable components (§1) +- [x] 2. Typed schema (§2) +- [x] 3. One real method projected, with provenance (§3) +- [x] 4. Dependency and validation queries, through Quack (§3) +- [x] 5. Optimization candidate from graph evidence: H1 (§6) +- [x] 6. Correctness oracle or falsifier for each claim (§4) +- [x] 7. MDL experiment with stated assumptions (§5) +- [x] 8. Capability ledger (§10) +- [x] 9. Plan for G1 compositions without a second registry (§8) +- [ ] Multi-hop questions (blocked, §9) +- [ ] Persisting the graph as a Lance dataset; source revisions from git + +## 1. Inventory: what exists, read in code + +### Multilingual Bible work + +**No seven-language graph exists in code.** + +What does exist: +- **The KJV in English, as triples.** + - `deepnsm-v2/examples/bible_wave.rs`. + - Stored as `Spo` packed into 48 bits, kept in a `TemporalStream` whose + version number is the verse index. + - No provenance per triple; no snapshots. +- **A lexeme fixture across 6 lanes.** + - `lance-graph-planner/examples/probe_babel_stances.rs` plus + `data/babel/lanes.rs`. + - Lanes: English, German, Czech, Latin, Greek and Aramaic, 6 rows each. + - A concept is a WordNet synset (`Coord`). A surface form (`LaneLex`) is + linked to its concept only by a string `pair` plus a verse address. +- **German silver labels**, from an optional external directory. + +The figure "9 lanes / 7 languages" appears only in +`.claude/plans/cycle-loop-closure-driver-v1.md`, and that plan itself retracts +the claim that the probe's lanes were corpora. No Bible query runs through +Quack. + +**Reused here: the principle, not the machinery.** An identity is separate from +its spellings: +- A method is a 16-byte `MethodKey`; its label is display text only. +- A field is (concept, schema seal, canonical name), never a label or an + ordinal. + +### Identity contracts + +| concept | identity | where | minted by | +|---|---|---|---| +| class | u16 codebook id | `ogar-vocab` `Class::canonical_id`, `class_ids` | OGAR | +| field | canonical name of the class's `ClassView`, in basis order | `basis::canonical_fields()` → `OgarClassView::fields` | OGAR | +| schema | `SchemaSeal` (FNV over the basis) | `method.rs:72` | computed locally | +| method | `MethodKey([u8;16])` | `method.rs:66` | **caller-issued; OGAR mints no method id** (`ActionDef` is keyed by an identity string; `ogar-loco` `Inventory::key_of` is unconnected) | +| reading | `ReadingId(u16)` | `method.rs:94` | literal in tests | +| effect | `EffectClass` | `method.rs:104` (the enum OGAR #357 proposes) | — | + +**OGAR #357** exists only locally, as the branch `pr357`. It is documentation +only (`ALGEBRAIC-LOGIC-COMPILER-RFC.md` plus a falsification document), and +the `LogicMethodSpec` it proposes is not coded. Its line 36 calls the function +inventory "not implemented", but `ogar-loco/src/inventory.rs` already exists. + +### Provenance pattern (dir-sim) + +`ogar-dir-sim/src/provenance.rs` provides: +- `VersionId`, a logical clock; +- `RuleId { name, version }`, where a behaviour change is a new version, never + an edit; +- `Origin::{Observed, Simulated}`. + +The graph's `Tier` follows the same rule: how a fact is known is part of the +fact. + +### Query substrate + +The edge table is a Quack table. Ingredients: +- **Binder:** `SoftwareGraph` implements `quack::bind::Binder`. +- **Front end:** `Draft`, the ordinary one. +- **Terminal:** `Agg::ScatterOrU32`, the one-hop result. + +Additional facts read in code: +- **mask-risc `Program::lowering()`** is derived from the program text: `Range`, + `Ternlog`, … or `Tiled`. `FusedFold` has only `Count` and `Any`, and + `fused_ternlog` declines any `Pred`. +- **#1311's traversal adapter** is test code: Quack has no edge type and no hop + operator, only public building blocks. + +## 2. Schema + +### Nodes + +A node's kind is its identity prefix (`kind_of`). One identity has exactly one +kind; interning asserts it, and `claim` refuses an unknown prefix. + +| kind | identity | rule | +|---|---|---| +| class | `class:0x0103` | OGAR codebook id | +| method | `method:` | never the label | +| field | `field://` | the same label in two classes gives two nodes | +| cond | `cond:=@r` | the reading is part of the identity | +| fold | `fold:sum()@r`, `fold:count(*)@r…` | bag `COUNT(*)` only; no set fold exists in a method body | +| law | `law:values`, `law:nars_revision(...)` | | +| program | `program:#` | | +| primitive | `prim:` | read from the program | +| lowering | `lowering:Tiled` … | `Program::lowering()` | +| refusal | `refusal:` | | +| claim | `claim::` | | +| capability | `cap:` | | +| test | `test:::` | | + +### Edges + +Each edge is a row `(src, rel, dst, tier, ctx, prov)`. +- `ctx` is the method (or class) whose projection wrote the edge. +- `prov` points to (file, revision, seal, reading). + +| rel | direction | written at | +|---|---|---| +| HAS_METHOD | class → method | declared | +| READS_FIELD, FILTERS, FOLDS, FINISHED_BY | method → field / cond / fold / law | declared | +| DEPENDS_ON_CLAIM | field → claim | derived | +| COMPUTES_FROM | field → field | derived | +| LOWERS_TO | method → program | compiled | +| USES_PRIMITIVE | program → primitive | compiled; on any other tier it is a *claim* and is checked (falsifier 8) | +| LOWERS_AS | program → lowering | compiled | +| REFUSED_FOR | method or class → refusal | compiled (the compiler's verdict) | +| BLOCKED_BY | refusal → capability | compiled, and only when the refusal itself is compiled | +| MAY_REQUIRE | method → capability | hypothesis | +| VERIFIED_BY | method → test | verified, under a `Certificate` | + +The five tiers are kept apart: **declared, derived, compiled, verified, +hypothesis**. A recorded claim never becomes a verified capability by being +recorded. + +## 3. The experiment on #1504's methods + +The subjects are `count_billable`, `count_by_duration`, `count_not_billable`, +`billable_hours` (`COUNT(*)`, `SUM(duration)` WHERE `billable = true`) and +`hours_by_billable`. Each is projected with the `MethodTable` that prepared +it. Test names below are from `tests/awareness.rs`. + +- **Projection** (`billable_hours_is_projected_with_tiers_and_provenance`): + - class, fields, condition, folds, law; + - two programs, one per fold; + - primitives counted against the real program; + - claim grades; + - provenance. +- **A. Dependency** (`dependency_query_runs_through_quack_and_matches_the_programs`). + The query is: + ``` + edges WHERE rel = uses_primitive AND tier = compiled AND dst = prim:X + → ScatterOrU32(ctx) + ``` + - It is checked against the programs themselves for `MaskedSumI32`, `Count` + and `GroupSumI32`. + - A primitive that no program uses is not a node, so it does not bind. + Nothing is minted. +- **B. Validation.** The verdicts are the compiler's; the queries read them + back. + - Under SAP's real claims, `billable_hours` is `refused_for UnmappedRequired`, + and its `duration` field `DEPENDS_ON_CLAIM claim:hours_logged:hypothesized`. + The graph explains a refusal down to the claim grade behind it + (`a_hypothesized_claim_refuses_the_method_and_the_graph_says_why`). + - Cross-class field labels, compute cycles and foreign readings: falsifiers + 1–3. +- **C. Capability gaps** (`a_declared_need_never_ships_a_capability`). + - `prim:MaskedSumI32` is shipped: a compiled program contains it. + - `cap:FirstOfRun`: + - starts as *claimed* (IAM and SAP needs as hypotheses, plus one + declared use); + - becomes *demonstrated* only after the compiler's + `EvidenceNotIndependent` refusal of NAL revision is projected and linked; + - a hand-noted refusal does not demonstrate it. +- **D. Repeated structure** (`repeated_structure_is_found_by_identity`). + - `cond:billable=true@r1` is shared by `count_billable` and `billable_hours`. + This is G1's `is_billable`, rediscovered from the rows. + - `!= true` is a different condition and is not counted as a repeat. + - `billable_hours` scans its selection once per fold. That is a + shared-selection candidate whose obligation stays open: one mask feeding + two terminals is a population intermediate the executor forbids, so it + needs a multi-terminal program. + +## 4. Falsifiers + +Every row has a test. Every guard was disabled once and its test went red +(14 disable runs; the tree was restored each time). + +| # | fault | outcome | test | disable that turned it red | +|---|---|---|---|---| +| 1 | same field label in two classes | two field nodes; `WrongConcept` | `the_same_field_label_in_two_classes_is_two_fields` | field identity without the concept | +| 2 | compute cycle | `refusal:ComputeCycle` from `compute_dag_is_acyclic` | `a_compute_cycle_is_the_contracts_refusal` | verdict not taken from the contract | +| 3 | identical folds under two readings | distinct nodes; `ReadingMismatch`; not paired | `identical_folds_under_two_readings_stay_apart` | identity without the reading | +| 4 | stale certificate | `StaleBody`, `StaleSeal` | `a_certificate_goes_stale_when_the_body_or_seal_changes` | digest check, seal check | +| 5 | bag vs set count | different terminal primitives over identical ops | `a_bag_count_and_a_set_count_are_different_primitives` | hop lands on `src` | +| 6 | fusion changes NULL behaviour | obligation open whenever a validity plane gates the program | `the_fusion_candidate_keeps_its_null_obligation_open_when_a_validity_plane_gates` | obligation discharged unconditionally | +| 7 | better on one workload, worse on another | `Verdict::Mixed`; a tie is not faster | `a_mixed_result_is_not_an_improvement` | "any faster" read as improves | +| 8 | claimed primitive not in the lowered program | `ClaimMismatch` | `a_claimed_primitive_must_be_in_the_lowered_program` | check ignores the owning method | +| 9 | capability "shipped" because a method declares it | `Claimed`, never `Shipped` | `a_declared_need_never_ships_a_capability` | shipped from any tier; `blocked_by` without a compiled refusal; kind guessed from the prefix | +| 10 | benchmark from one tier attributed to another | `TierMismatch` | `a_measurement_answers_only_for_its_own_tier` | tier ignored | + +**Two disable runs found two real defects.** +1. `blocked_by`'s compiled-refusal guard first stayed green: no test created a + refusal node that was not compiled. That gap was in the test. +2. Writing the missing test case exposed a defect in the code: `claim` picked a + node's kind from a narrow prefix check, so a hand-noted refusal became a + Capability node, and the compiler's later refusal reused that wrong-kind + node. Fixed with `kind_of` (`eaa58f6`). + +## 5. Entropy and MDL + +There are three separate measurements, never one number: +- **Structural redundancy:** `shared_predicates`, `shared_selections`. +- **Epistemic uncertainty:** refusals traced to `hypothesized` claims, + `Claimed` vs `Demonstrated` capabilities, stale certificates. +- **Execution cost:** `Measurement`s per program, rows and tier. + +**Objective (research, not law):** `J = L(P) + L(M|P) + λC`. +- `description_length`: + - `L(P)` charges each atom `log2(atoms)` bits, and each composite one + `log2|V|` reference per atom it is defined over. + - `L(M|P)` charges each body token `log2|V|` bits. +- Correctness, authority, evidence independence and version compatibility are + **hard constraints**: a candidate that violates them is never scored. + +**Results** (`description_length_rewards_only_typed_multi_condition_reuse`): +- Factoring a **single** shared condition never shortens the description. G1's + one-condition `is_billable` has no syntactic description-length gain. Its + value is edit-once semantics: change it once and both consumers move. A + measure over syntax cannot see that. This is a stated limitation of the + measure, not of G1. +- Factoring a two-condition set shared by three methods does shorten it. +- **Counterexample** (identical labels, different meaning): the same three + bodies, read by label, repeat and appear to gain bits. Read by typed + identity (each class's own fields) they do not repeat, and factoring gains + nothing. A label-level detector would propose a cross-class factoring that + the typed graph refuses. +- **The first metric was wrong, and its own counterexample caught it.** + Defining a composite removed its atoms from the vocabulary, so even a + single-use factoring looked profitable. A composite is spelled in atoms, so + its atoms stay in P. + +The λC term is **unscored** for H1: the cost after fusion has not been +measured (§6), and a scored J would invent it. + +## 6. H1: fuse the predicate into the masked sum + +**What the graph found.** `billable_hours`'s sum program: +- uses `MaskedSumI32`; +- has `LOWERS_AS lowering:Tiled`; +- reads only the live plane. + +Generated by `fusion_candidates`. + +**Measured with `examples/awareness_loop.rs`** (1,048,576 rows, 51 reps, +interleaved; the answers are asserted equal before anything is timed). The +machine is shared and noisy. + +| build | detected AVX-512 | program median | scalar reference median | ratio | +|---|---|---|---|---| +| native (`avx512f` compiled) | yes | 603.5 µs | 379.1 µs | 1.59 | +| native, second run | yes | 619.6 µs | 362.8 µs | 1.71 | +| `config-v3` (`avx512f` not compiled) | yes | 617.4 µs | 347.6 µs | 1.78 | +| #1504 ledger, x86-64-v3 | — | 600 µs | 366 µs | 1.64 | + +- **Observed:** the tiled program costs 1.6–1.8× a single-pass scalar loop on + both tiers. +- **Hypothesized, not shown:** a fused mask-risc terminal would close that gap. + The scalar loop is a reference for what one pass costs, not a fused SIMD + terminal. + +**What is actually missing.** Two pieces, both in mask-risc's recognisers: +1. Fusion over a lane `Pred`: `fused_ternlog` declines any `Pred`. +2. A sum fold: `FusedFold` is only `Count` and `Any`. + +**Obligations:** +- NULL/UNKNOWN: every validity plane must still gate the sum. Discharged here, + because the program reads only the live plane. +- The `MASKED_SUM_I32_MAX_ROWS` bound. +- The fused terminal itself. + +**Falsifiers that already exist:** the row-loop oracles in `tests/method.rs` +and `tests/awareness.rs`, and the Quack == R2IL parity. + +**Who would benefit:** every program the dependency query returns for +`MaskedSumI32` with `lowering:Tiled`. + +## 7. Missing algebra from repeated needs + +`FirstOfRun` (a first-occurrence mask over an ordered key) is the shared +candidate. +- **NAL revision:** the need is demonstrated by a compiler refusal. +- **IAM reconcile and SAP import:** recorded as `MAY_REQUIRE` hypotheses. + Neither was checked against IAM or SAP code here. + +A shared physical primitive does not equate the domain laws. NARS evidence +independence, IAM identity uniqueness and SAP transaction identity remain +separate laws, each needing its own proof. The graph may recommend the +primitive; it records no semantic equivalence. + +## 8. The loop, and integrating G1 + +**States:** candidate → structurally valid → semantically verified → +performance measured → rejected / approved for implementation. + +**Executable today:** +- projection; +- queries A to D; +- compiler verdicts; +- certificates; +- tier-labelled measurements; +- verdicts across workloads. + +**Not executable today:** acting on a candidate. Nothing rewrites code, a +declaration, a deployment or an external system. + +**G1 (#1508)** needs no second registry. `Cond::Method` references become +`CALLS` edges (method → method, declared), and the compiler's +`MethodCycle`, `MethodRefContract`, `MethodRefNotPredicate` and +`UnknownMethodRef` refusals become `REFUSED_FOR` edges. The projection keeps +reading the `MethodTable`; the graph never resolves a method. + +A separate hypothesis, not attempted here: a graph representation *as* a +method body. That would need its own parity test against +`MethodTable::prepare`. + +## 9. Blockers and gaps + +1. **Multi-hop.** "Methods using X" is one hop only because every edge + carries `ctx`, the method it was written within. + - "Methods two calls away from X" would need a mask from one program fed to + the next. Quack documents that as a forbidden population intermediate + (`Filter::Semijoin`). + - The legal routes are the factored `EqU32Via` read or a relation carrier + (D-CMH-G1-4). + - **This is the first concrete blocker.** It is not worked around with a + separate traversal engine. +2. **Method identity is caller-issued.** OGAR mints no method key. +3. **The body digest** is FNV over a spec's `Debug` rendering. + - It is stable within one build. + - A toolchain change makes every certificate stale (never falsely fresh). + - A canonical byte encoding of `MethodSpec` would remove that. +4. **The graph is in memory.** It is not yet a Lance dataset, and the source + revision is a caller string (`SAG_REV`), not read from git. +5. **Certificates are issued by tests,** not by a running service. + +## 10. Capability ledger + +| item | status | evidence | +|---|---|---| +| projection of prepared methods, tiers, provenance | implemented | 16 tests | +| dependency query through Quack | implemented, one hop | query A test; example output | +| compiler verdicts as edges (refusals, compute cycle) | implemented | falsifiers 1–3, query B | +| certificates and staleness | implemented | falsifier 4 | +| capability status | implemented | falsifier 9 | +| shared predicates / shared selections | implemented (detection only) | query D test | +| MDL measure | implemented, crude, with its limitation | MDL test | +| H1 fusion candidate | candidate; cost gap measured; fused terminal absent | `awareness_loop` | +| `FirstOfRun` | demonstrated gap (NAL); hypothesis (IAM, SAP) | query C test | +| multi-hop dependency | blocked | §9.1 | +| applying a transformation | out of scope | — | From 78b6b73b327d0a7cdf2f5469f17bf34a76650568 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:18:13 +0000 Subject: [PATCH 5/7] glove-parity: awareness test, array instead of vec (clippy useless_vec) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/lance-graph-glove-parity/tests/awareness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lance-graph-glove-parity/tests/awareness.rs b/crates/lance-graph-glove-parity/tests/awareness.rs index bb9f13444..2533f8c66 100644 --- a/crates/lance-graph-glove-parity/tests/awareness.rs +++ b/crates/lance-graph-glove-parity/tests/awareness.rs @@ -1046,7 +1046,7 @@ fn method_references_are_calls_edges_and_cycles_are_the_compilers() { // detect cycles itself. let mut cyc = w.graph.refused_for("MethodCycle").unwrap(); cyc.sort_unstable(); - let mut want = vec![method_ident(A), method_ident(B)]; + let mut want = [method_ident(A), method_ident(B)]; want.sort(); assert_eq!(cyc, want.iter().map(String::as_str).collect::>()); assert_eq!(w.graph.out(&method_ident(A), Rel::Calls).len(), 1); From 986ea8ab86a9ab6b93bf309b2cd49c0ad70746df Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:18:49 +0000 Subject: [PATCH 6/7] board: D-SAG-1-5 (G1 CALLS edges) in PR Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 2 +- .claude/plans/software-awareness-graph-v1.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 12843f06a..eca85f627 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -9,7 +9,7 @@ Plan: `.claude/plans/software-awareness-graph-v1.md`. No new opcode, Quack varia | D-SAG-1-2 | queries A–D through Quack; compiler verdicts as edges | In PR | answers == the programs; falsifiers 1–10, each disable-verified | | D-SAG-1-3 | H1 candidate + tier-labelled measurement; MDL measure with counterexample | In PR | `examples/awareness_loop.rs`; MDL test | | D-SAG-1-4 | multi-hop dependency via relation carrier / factored `EqU32Via` | Queued (needs D-CMH-G1-4) | two-hop answer == closure computed from the programs | -| D-SAG-1-5 | G1 `CALLS` edges + method-reference refusals | Queued (#1508 merged) | `MethodCycle` etc. recorded, never re-derived | +| D-SAG-1-5 | G1 `CALLS` edges + method-reference refusals | In PR | `method_references_are_calls_edges_and_cycles_are_the_compilers`; callers via Quack; `MethodCycle` recorded, never re-derived | | D-SAG-1-6 | persist as a Lance dataset; source revision from git; canonical body digest | Queued | reload equals projection; toolchain change keeps digests | ## D-CMH-G1 — Gremlin as a composable ClassId algebra (2026-10-12) diff --git a/.claude/plans/software-awareness-graph-v1.md b/.claude/plans/software-awareness-graph-v1.md index d0f3ede83..b635815aa 100644 --- a/.claude/plans/software-awareness-graph-v1.md +++ b/.claude/plans/software-awareness-graph-v1.md @@ -33,6 +33,7 @@ propose a factoring. Nothing in this plan concerns hardware. - [x] 7. MDL experiment with stated assumptions (§5) - [x] 8. Capability ledger (§10) - [x] 9. Plan for G1 compositions without a second registry (§8) +- [x] G1 method references as `CALLS` edges; callers through Quack; `MethodCycle` recorded (§8) - [ ] Multi-hop questions (blocked, §9) - [ ] Persisting the graph as a Lance dataset; source revisions from git @@ -336,7 +337,7 @@ performance measured → rejected / approved for implementation. **Not executable today:** acting on a candidate. Nothing rewrites code, a declaration, a deployment or an external system. -**G1 (#1508)** needs no second registry. `Cond::Method` references become +**G1 (#1508, merged) — done in this PR (D-SAG-1-5).** It needs no second registry. `Cond::Method` references become `CALLS` edges (method → method, declared), and the compiler's `MethodCycle`, `MethodRefContract`, `MethodRefNotPredicate` and `UnknownMethodRef` refusals become `REFUSED_FOR` edges. The projection keeps From b3e3b3ae64d8e94f6a037a741ff9ac80c4bcfc30 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 18:25:14 +0000 Subject: [PATCH 7/7] glove-parity: claims cannot write compiled/verified edges; re-projection replaces CodeRabbit on #1509. claim() wrote at any tier, so a note at Tier::Compiled could ship a capability (capability() reads the tier) or demonstrate a gap (blocked_by() accepts any compiled RefusedFor). It now refuses Compiled and Verified; only project, add_program, project_compute, blocked_by and certify write those. project() only added edges, so re-projecting a key kept its old structure, programs and VerifiedBy next to the new verdict; it now drops every edge written within the method before re-linking. Tests a_claim_cannot_take_the_compiled_or_verified_tier and a_reprojection_replaces_the_methods_earlier_edges, both red before the fix. Plan and board counts updated (19 awareness tests; D-SAG-1-5 in the PR). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/INTEGRATION_PLANS.md | 2 +- .claude/plans/software-awareness-graph-v1.md | 7 +- .../lance-graph-glove-parity/src/awareness.rs | 14 +++- .../tests/awareness.rs | 78 +++++++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 29135d24b..2271ba337 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,6 +1,6 @@ ## 2026-10-12 (4) — software-awareness-graph-v1 — the software's own methods, programs and evidence as graph rows → `.claude/plans/software-awareness-graph-v1.md` -**Status:** MEASURED for one concept's methods (D-SAG-1-0..3 in the PR); D-SAG-1-4..6 queued. +**Status:** MEASURED for one concept's methods (D-SAG-1-0..3 and D-SAG-1-5 in the PR); D-SAG-1-4 and D-SAG-1-6 queued. - A `MethodTable` projects to typed rows (class, method, field, condition, fold, law, program, primitive, lowering, refusal, claim, capability, test). Each row has a tier (declared / derived / compiled / verified / hypothesis) and a provenance. - The edge table is a Quack table: dependency questions are Drafts lowered and run on mask-risc. Validation is the compiler's verdicts recorded as edges. diff --git a/.claude/plans/software-awareness-graph-v1.md b/.claude/plans/software-awareness-graph-v1.md index b635815aa..e5783aaae 100644 --- a/.claude/plans/software-awareness-graph-v1.md +++ b/.claude/plans/software-awareness-graph-v1.md @@ -2,7 +2,7 @@ **Status:** MEASURED for one concept's methods (#1504's `BillableWorkEntry` set). Code: `crates/lance-graph-glove-parity/src/awareness.rs`, tests -`tests/awareness.rs` (16), example `examples/awareness_loop.rs`. +`tests/awareness.rs` (19), example `examples/awareness_loop.rs`. **Research question.** Can lance-graph represent its own methods, dependencies, semantic contracts and execution evidence as graph rows in its existing @@ -371,11 +371,12 @@ method body. That would need its own parity test against | item | status | evidence | |---|---|---| -| projection of prepared methods, tiers, provenance | implemented | 16 tests | +| projection of prepared methods, tiers, provenance | implemented | 19 tests | | dependency query through Quack | implemented, one hop | query A test; example output | | compiler verdicts as edges (refusals, compute cycle) | implemented | falsifiers 1–3, query B | | certificates and staleness | implemented | falsifier 4 | -| capability status | implemented | falsifier 9 | +| capability status | implemented; a note cannot write a compiled or verified edge | falsifier 9; `a_claim_cannot_take_the_compiled_or_verified_tier` | +| re-projection replaces a method's edges | implemented | `a_reprojection_replaces_the_methods_earlier_edges` | | shared predicates / shared selections | implemented (detection only) | query D test | | MDL measure | implemented, crude, with its limitation | MDL test | | H1 fusion candidate | candidate; cost gap measured; fused terminal absent | `awareness_loop` | diff --git a/crates/lance-graph-glove-parity/src/awareness.rs b/crates/lance-graph-glove-parity/src/awareness.rs index d9d71116c..d0a762175 100644 --- a/crates/lance-graph-glove-parity/src/awareness.rs +++ b/crates/lance-graph-glove-parity/src/awareness.rs @@ -584,6 +584,10 @@ impl SoftwareGraph { self.digests.push((spec.key, digest(spec), spec.schema)); let class = self.intern(Kind::Class, format!("class:{:#06x}", spec.concept)); let m = self.intern(Kind::Method, method_ident(spec.key)); + // A method's projection replaces its earlier one: every edge + // written within it (structure, programs, verdicts, + // certificates, notes) belongs to the body it described. + self.edges.retain(|e| e.ctx != m); self.nodes[m as usize].label = Some((*label).to_owned()); self.link(class, Rel::HasMethod, m, Tier::Declared, m, prov); let r = spec.reading.0; @@ -711,7 +715,8 @@ impl SoftwareGraph { /// /// # Errors /// - /// The identity whose prefix names no kind. + /// A compiled or verified tier, or an identity whose prefix names no + /// kind. pub fn claim( &mut self, src: &str, @@ -720,6 +725,13 @@ impl SoftwareGraph { tier: Tier, prov: u32, ) -> Result<(), String> { + // Compiled and verified edges are written by `project`, `add_program`, + // `project_compute`, `blocked_by` and `certify`, never by a note. + if matches!(tier, Tier::Compiled | Tier::Verified) { + return Err(format!( + "{tier:?} is written by the compiler or a certificate" + )); + } let m = self.intern_ident(src)?; let c = self.intern_ident(target)?; self.link(m, rel, c, tier, m, prov); diff --git a/crates/lance-graph-glove-parity/tests/awareness.rs b/crates/lance-graph-glove-parity/tests/awareness.rs index 2533f8c66..1dfa2dcb6 100644 --- a/crates/lance-graph-glove-parity/tests/awareness.rs +++ b/crates/lance-graph-glove-parity/tests/awareness.rs @@ -1051,3 +1051,81 @@ fn method_references_are_calls_edges_and_cycles_are_the_compilers() { assert_eq!(cyc, want.iter().map(String::as_str).collect::>()); assert_eq!(w.graph.out(&method_ident(A), Rel::Calls).len(), 1); } + +/// FAILS IF: a claim can be written at a tier only the compiler or a +/// certificate may write — which would let a note ship a capability or +/// demonstrate a gap — or a refused claim leaves an edge behind. +#[test] +fn a_claim_cannot_take_the_compiled_or_verified_tier() { + let mut g = SoftwareGraph::new(); + let p = g.note(SOURCE, REV); + for tier in [Tier::Compiled, Tier::Verified] { + assert!(g + .claim("method:m", Rel::UsesPrimitive, "cap:FirstOfRun", tier, p) + .is_err()); + assert!(g + .claim( + "method:a_note", + Rel::RefusedFor, + "refusal:EvidenceNotIndependent", + tier, + p + ) + .is_err()); + } + assert!(g.edges().is_empty()); + assert_eq!(g.capability("cap:FirstOfRun"), CapStatus::Unknown); + assert!(g + .blocked_by("refusal:EvidenceNotIndependent", "cap:FirstOfRun", p) + .is_err()); + // Silence twin: the claim tiers still record. + for tier in [Tier::Declared, Tier::Derived, Tier::Hypothesis] { + g.claim("method:m", Rel::MayRequire, "cap:FirstOfRun", tier, p) + .unwrap(); + } + assert_eq!(g.edges().len(), 3); +} + +/// FAILS IF: projecting a method again keeps its earlier projection's edges: +/// stale conditions, a stale program next to a new refusal, or a +/// verification of a body that no longer exists. +#[test] +fn a_reprojection_replaces_the_methods_earlier_edges() { + let b = batch(65); + let basis = canonical_fields(); + let cats = CatsBinder::new(&b); + let claims = with_duration(); + let first = specs(); + let t1 = MethodTable::prepare(&glove(&basis, &cats, &claims), &first); + let mut g = SoftwareGraph::new(); + g.project(&t1, &first, &claims, SOURCE, REV); + let p = g.note(SOURCE, REV); + g.certify( + &Certificate { + method: BILLABLE_HOURS, + digest: g.digest(BILLABLE_HOURS).unwrap(), + seal: seal(), + test: "x".into(), + }, + p, + ) + .unwrap(); + assert_eq!(g.out(&hours_ident(), Rel::VerifiedBy).len(), 1); + // The same key, now reading a field the glove cannot bind: refused. + let mut second = specs(); + second[3].1.body = query(&[BILLABLE_TRUE], &[Fold::Sum("activity_type")]); + let t2 = MethodTable::prepare(&glove(&basis, &cats, &claims), &second); + assert!(t2.get(BILLABLE_HOURS).unwrap().is_err()); + g.project(&t2, &second, &claims, SOURCE, REV); + let m = hours_ident(); + assert!(g.out(&m, Rel::VerifiedBy).is_empty()); + assert!(g.out(&m, Rel::LowersTo).is_empty()); + assert_eq!(g.out(&m, Rel::RefusedFor).len(), 1); + assert_eq!(g.out(&m, Rel::Folds).len(), 1); + assert!(!g + .methods_using("MaskedSumI32") + .unwrap() + .contains(&m.as_str())); + // Other methods' edges are untouched. + assert_eq!(g.out(&method_ident(COUNT_BILLABLE), Rel::LowersTo).len(), 1); +}