Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/board/ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Issues Log — Open + Resolved (double-entry, append-only)

## 2026-07-07 — ISS-V1-TAIL-RESIDUE PROGRESS — structural fix landed; aiwar V3-classid flip now blocked on a consumer, not the feature gate

**Status:** PARTIALLY RESOLVED. The mandatory *structural* half of ISS-V1-TAIL-RESIDUE (below) is done in one PR:
- **`mint_for` un-gated** (`canonical_node.rs`): moved to an unconditional `impl NodeGuid` — V1 arm always available, V2/V3 arms feature-gated with a dead V1 fallback so `--no-default-features` still compiles.
- **`guid-v3-tail` default-on** (`lance-graph-contract/Cargo.toml`), per the operator ruling. 854 lib tests green on default; `--no-default-features` still compiles; downstream `lance-graph` + `lance-graph-planner` check clean.
- **Both mint sites route through `mint_for`** now — `ocr.rs` (`mint_for(classid_read_mode(classid).tail_variant, …)`, classid-param-driven) and `aiwar.rs` (`osint_classid`, still `CLASSID_OSINT`). No hardcoded `NodeGuid::new` remains in either production path.
- **`/v3-audit` check #6** added: forbids `NodeGuid::new(` in non-test production code (the mechanical guard).

**Newly discovered blocker (was NOT in the original issue).** Flipping `aiwar` to `CLASSID_OSINT_V3` (a real V3 mint) **broke `aiwar::tests::projects_to_family_node_graph`**: the V3 tail lays `family` at bytes 12..14 (u16), but `soa_graph::project_snapshot` reads `family` via the **V1 `family()` u24 accessor** (bytes 10..13) → family grouping reads garbage (leaf-dominated). So aiwar stays on the V1 `CLASSID_OSINT` (behavior-preserving via `mint_for`); its V3-classid flip is now gated on making `project_snapshot` (and any `family()`-reading consumer) **tail-aware** — an I-LEGACY-API-FEATURE-GATED consumer migration, not the feature gate the original issue named. Once `CLASSID_OSINT`'s registry `tail_variant` flips, the aiwar mint auto-follows.

**Remaining (still OPEN):** (1) make `project_snapshot`/`OSINT_GOTHAM` read family tail-aware; (2) then flip `CLASSID_OSINT` (or point aiwar/ocr at a V3-marked classid). ocr is already V3-ready (classid-param-driven through `mint_for`).

## 2026-07-04 — ISS-V1-TAIL-RESIDUE — two pre-existing `NodeGuid::new` (V1 `u24+u24`) mint sites must migrate to V3 (`mint_for` / V3-marked classid)

**Status:** OPEN — **MIGRATION MANDATORY** (operator ruling 2026-07-04, `E-V1-TAIL-FORBIDDEN-V3-IS-CONTENT-BLIND-1`). Deferred in *timing*, not in *obligation*; NOT to be churned into unrelated PRs. Owner: whoever next moves each output path onto a V3-marked classid.
Expand Down
6 changes: 6 additions & 0 deletions .claude/commands/v3-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ every hit with file:line and a one-line disposition
— verify const size asserts unchanged or `ENVELOPE_LAYOUT_VERSION`
bumped, and that no `_LEGACY` read-mode key was removed (retirement is
corpus-proof-gated).
6. **V1-mint forbid** (ISS-V1-TAIL-RESIDUE): pattern `NodeGuid::new\(`
in non-test production code is a violation — new mints route through
`NodeGuid::mint_for(classid_read_mode(c).tail_variant, …)` so the
class registry drives the tail. Sanctioned: `#[cfg(test)]` modules,
`mint_for`'s own V1 arm (`canonical_node.rs`), and legacy-compat
*reads* (`family()`/`identity()` accessors are reads, not mints).

End with a verdict per the v3-mailbox-warden vocabulary (OWNED /
BOOTSTRAP-OK / ORPHAN-WRITE / RESURRECTION) plus LAYOUT-CLEAN /
Expand Down
5 changes: 5 additions & 0 deletions crates/lance-graph-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ name = "i4_batch"
harness = false

[features]
# default-on `guid-v3-tail` (operator ruling 2026-07-04, ISS-V1-TAIL-RESIDUE):
# every normal build mints real V3 keys via `mint_for`. `--no-default-features`
# still compiles (mint_for's V1 arm is un-gated; V2/V3 arms fall back to V1).
default = ["guid-v3-tail"]

# A-unlock-stepdomain — `step_trajectory_hash` forward stub for the E4
# cross-PR bridge between PR #278 audit log + PR #279 grammar trajectory.
# No-op alias today; concrete impl lands once the bridge PR ships.
Expand Down
20 changes: 17 additions & 3 deletions crates/lance-graph-contract/src/aiwar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! `GraphSnapshot` to the Quadro-2 visual. Run it on the real graph with
//! `cargo run -p lance-graph-contract --example aiwar_family_poc`.

use crate::canonical_node::{EdgeBlock, NodeGuid, NodeRow};
use crate::canonical_node::{classid_read_mode, EdgeBlock, NodeGuid, NodeRow};
use crate::literal_graph::LiteralGraph;
use std::collections::{BTreeMap, BTreeSet};

Expand Down Expand Up @@ -72,6 +72,14 @@ impl AiwarClassView {
/// projected graph links category hubs (Nation → System, etc.). Head-only — the
/// 480-byte value slab stays zero.
pub fn aiwar_node_rows(graph: &LiteralGraph) -> Vec<NodeRow> {
// Route through `mint_for` (ISS-V1-TAIL-RESIDUE) — never a hardcoded `new`.
// Kept on the V1 `CLASSID_OSINT` for now: flipping to `CLASSID_OSINT_V3`
// gives the V3 `leaf·family·identity` tail, but `project_snapshot` still
// reads `family` via the V1 `family()` u24 accessor, so the V3 flip is
// gated on making that consumer tail-aware (a follow-up). Once
// `CLASSID_OSINT`'s registry `tail_variant` flips, this mint auto-follows.
let osint_classid = NodeGuid::CLASSID_OSINT;

let view = AiwarClassView::from_graph(graph);
let ids = graph.all_node_ids();
let fam_of =
Expand Down Expand Up @@ -101,8 +109,14 @@ pub fn aiwar_node_rows(graph: &LiteralGraph) -> Vec<NodeRow> {
edges.out_family[k] = b;
}
NodeRow {
key: NodeGuid::new(
NodeGuid::CLASSID_OSINT,
// Route through `mint_for` so the classid's registered
// `tail_variant` drives the key layout (ISS-V1-TAIL-RESIDUE).
// V1 today ⇒ family/identity keep the u24 mask; a V3 registry
// flip would narrow them to u16 automatically.
key: NodeGuid::mint_for(
classid_read_mode(osint_classid).tail_variant,
osint_classid,
0,
0,
0,
0,
Expand Down
121 changes: 64 additions & 57 deletions crates/lance-graph-contract/src/canonical_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,68 @@ impl NodeGuid {
}
}

impl NodeGuid {
/// Mint a node by its **tail variant** — the carrier form of the Phase-1
/// symmetric spine: a consumer mints with
/// `mint_for(classid_read_mode(c).tail_variant, …)`, NEVER by hardcoding
/// `new` vs `new_v2`. Migrating a class's identity to V3 is then a one-line
/// flip of its `tail_variant` in the registry, with zero consumer rewrite.
///
/// **Un-gated dispatch (`ISS-V1-TAIL-RESIDUE`):** `mint_for` itself is always
/// available so production mints (`ocr.rs`, `aiwar.rs`) route through it in
/// every build. The [`V1`](TailVariant::V1) arm is unconditional
/// ([`new`](NodeGuid::new)); the [`V2`](TailVariant::V2)/[`V3`](TailVariant::V3)
/// arms lower to `new_v2` only under `guid-v2-tail`. With the feature off no
/// classid registers a V2/V3 `tail_variant` ([`classid_read_mode`] returns
/// V1), so the fallback arm is dead — it exists purely so the crate compiles
/// `--no-default-features`.
///
/// **No silent truncation:** the V2/V3 arm asserts `family`/`identity` fit
/// `u16`, mirroring [`new`](NodeGuid::new)'s own 24-bit guard.
#[allow(clippy::too_many_arguments)]
pub const fn mint_for(
tail_variant: TailVariant,
classid: u32,
heel: u16,
hip: u16,
twig: u16,
leaf: u16,
family: u32,
identity: u32,
) -> Self {
match tail_variant {
TailVariant::V1 => Self::new(classid, heel, hip, twig, family, identity),
#[cfg(feature = "guid-v2-tail")]
TailVariant::V2 | TailVariant::V3 => {
assert!(
family <= 0xFFFF,
"v2/v3 family must fit in 16 bits (no silent truncation)"
);
assert!(
identity <= 0xFFFF,
"v2/v3 identity must fit in 16 bits (no silent truncation)"
);
Self::new_v2(
classid,
heel,
hip,
twig,
leaf,
family as u16,
identity as u16,
)
}
#[cfg(not(feature = "guid-v2-tail"))]
TailVariant::V2 | TailVariant::V3 => {
// feature off ⇒ V2/V3 unreachable (no classid registers them);
// fall back to the V1 layout so the crate compiles.
let _ = leaf;
Self::new(classid, heel, hip, twig, family, identity)
}
}
}
}

// ── GUID v2 tail (leaf·family·identity, 3×u16) — D-GV2-1, feature-gated ────────
//
// The v2 basin tail repartitions bytes 10..16: leaf(u16) 10..12 (the 4th HHTL
Expand Down Expand Up @@ -353,63 +415,8 @@ impl NodeGuid {
])
}

/// Mint a node by its **tail variant** — the carrier form of the Phase-1
/// symmetric spine (`soa-value-tenant-migration-v2.md` §2.1): a consumer
/// mints with `mint_for(classid_read_mode(c).tail_variant, …)`, NEVER by
/// hardcoding `new` vs `new_v2`. The key-side analog of the value-side
/// `to_node_row(classid_read_mode(c).value_schema, …)` — same
/// [`classid_read_mode`] lookup, sibling field. Migrating a class's identity
/// to V3 is then a one-line flip of its `tail_variant` in the registry, with
/// zero consumer rewrite (the "extend the one `ReadMode`, never a public
/// `new_v3`" litmus).
///
/// Dispatch (all three [`TailVariant`] arms exist unconditionally as enum
/// values; only the constructors they call are gated):
/// - [`V1`](TailVariant::V1) → [`new`](NodeGuid::new): the canonical
/// `family(u24)·identity(u24)` tail. `leaf` is not part of the V1 tail and
/// is intentionally ignored (the V1 cascade is HEEL·HIP·TWIG only).
/// - [`V2`](TailVariant::V2) / [`V3`](TailVariant::V3) → [`new_v2`](NodeGuid::new_v2):
/// the shared `leaf·family·identity` 3×u16 tail bytes. V3 differs from V2
/// only in how those bytes are *read* (the `(part_of:is_a)` cascade tile),
/// not how they are *stored* — so it mints through the same constructor.
///
/// **No silent truncation** (the footgun v2 exists to remove): the V2/V3 arm
/// asserts `family`/`identity` fit `u16`, mirroring [`new`](NodeGuid::new)'s
/// own 24-bit guard. An out-of-range value is a loud panic, never a wrong key.
#[allow(clippy::too_many_arguments)]
pub const fn mint_for(
tail_variant: TailVariant,
classid: u32,
heel: u16,
hip: u16,
twig: u16,
leaf: u16,
family: u32,
identity: u32,
) -> Self {
match tail_variant {
TailVariant::V1 => Self::new(classid, heel, hip, twig, family, identity),
TailVariant::V2 | TailVariant::V3 => {
assert!(
family <= 0xFFFF,
"v2/v3 family must fit in 16 bits (no silent truncation)"
);
assert!(
identity <= 0xFFFF,
"v2/v3 identity must fit in 16 bits (no silent truncation)"
);
Self::new_v2(
classid,
heel,
hip,
twig,
leaf,
family as u16,
identity as u16,
)
}
}
}
// `mint_for` moved to the unconditional `impl NodeGuid` above (un-gated per
// ISS-V1-TAIL-RESIDUE): V1 arm always available, V2/V3 arms feature-gated.

/// v2 `leaf` — bytes 10..12, the 4th HHTL routing tier (cascade terminal).
#[inline]
Expand Down
17 changes: 14 additions & 3 deletions crates/lance-graph-contract/src/ocr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,20 @@ impl<'a> LayoutBlock<'a> {

NodeRow {
// HHT unbound (0) and default basin for the POC — only `identity`
// discriminates (the canon bootstrap address); `classid` still selects
// the read-mode. Minting HEEL/HIP/TWIG + family is the OGAR follow-up.
key: NodeGuid::new(classid, 0, 0, 0, NodeGuid::FAMILY_DEFAULT, identity),
// discriminates (the canon bootstrap address). Route through
// `mint_for` so the classid's registered `tail_variant` (V1 or V3)
// drives the key layout — never a hardcoded `new` (ISS-V1-TAIL-RESIDUE).
// Minting HEEL/HIP/TWIG + family is the OGAR follow-up.
key: NodeGuid::mint_for(
classid_read_mode(classid).tail_variant,
classid,
0,
0,
0,
0,
NodeGuid::FAMILY_DEFAULT,
identity,
),
edges: EdgeBlock::default(),
value,
}
Expand Down
Loading