Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .claude/board/ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Issues Log — Open + Resolved (double-entry, append-only)

## 2026-07-07 — ISS-V1-TAIL-RESIDUE — RESOLVED (un-gate + default-on + both mint sites V3-routed; aiwar mints real V3)

**Status:** RESOLVED. Landed in one PR (#663):
- **`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.
- **Both mint sites route through `mint_for`** — `ocr.rs` (classid-param-driven) and `aiwar.rs` (`CLASSID_OSINT_V3` in a normal build; V1 fallback under `--no-default-features`). No hardcoded `NodeGuid::new` remains in either production path.
- **`OSINT_GOTHAM` flipped to the V3 classid** (`soa_graph.rs`), so the projector's exact-classid filter matches the V3-minted aiwar rows; the `node()` test helper now mints via `mint_for` too.
- **`/v3-audit` check #6** forbids `NodeGuid::new(` in non-test production code.

**Correction of the mid-work diagnosis (recorded so it doesn't mislead).** An interim note in this entry claimed the aiwar V3 flip was blocked because `soa_graph::project_snapshot` reads `family` via the V1 `family()` u24 accessor. **That was WRONG** — `soa_graph`'s read path is already tail-aware (`family_of`/`identity_of` route through `classid_read_mode(guid.classid()).tail_variant` → `family_v2`/`identity_v2` for V3; proven by `v3_rows_decode_family_and_identity_via_tail_variant`). The *actual* blocker was a one-line domain-spec pin: `OSINT_GOTHAM.classid = CLASSID_OSINT` (V1), and `project_snapshot` filters rows by **exact** classid, so V3-minted rows were dropped (empty snapshot). Flipping the domain spec to the V3 classid fixed it. Consumers were V3-ready; the straggler was the domain constant, not the read path.

**Verification:** default build **854** lib tests green (V3), `--no-default-features` **840** green (V1 fallback), downstream `lance-graph` + `lance-graph-planner` check clean, fmt + clippy clean.

**Follow-up (separate, non-blocking):** `CLASSID_OSINT` (V1, `0x0700_0000`) remains a registered legacy alias — its retirement is corpus-proof-gated (W6), not part of this issue.

## 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
34 changes: 28 additions & 6 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,16 @@ 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> {
// ONE OSINT class, minted V3 (ISS-V1-TAIL-RESIDUE): route through `mint_for`
// so the classid's registered `tail_variant` drives the tail. `OSINT_GOTHAM`
// filters on the SAME selector, and `family_of`/`identity_of` decode the V3
// basin tail-aware — so the round-trip is consistent. `--no-default-features`
// falls back to the V1 `CLASSID_OSINT` (the V3 class is gated out there).
#[cfg(feature = "guid-v3-tail")]
let osint_classid = NodeGuid::CLASSID_OSINT_V3;
#[cfg(not(feature = "guid-v3-tail"))]
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,13 +111,19 @@ 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).
// Masked to u16 — fits both the V3 basin tail and the V1 fallback
// (POC cardinalities are tiny: category count + node count).
key: NodeGuid::mint_for(
classid_read_mode(osint_classid).tail_variant,
osint_classid,
0,
0,
0,
0,
fam & 0x00FF_FFFF,
(i as u32) & 0x00FF_FFFF,
fam & 0xFFFF,
(i as u32) & 0xFFFF,
),
edges,
value: [0u8; 480],
Expand Down Expand Up @@ -185,9 +201,15 @@ mod tests {

#[test]
fn rows_are_osint_class_and_head_only() {
// ONE OSINT class: V3 in a normal build, V1 under `--no-default-features`
// (mirrors `aiwar_node_rows` + `OSINT_GOTHAM`).
#[cfg(feature = "guid-v3-tail")]
let expected_classid = NodeGuid::CLASSID_OSINT_V3;
#[cfg(not(feature = "guid-v3-tail"))]
let expected_classid = NodeGuid::CLASSID_OSINT;
let g = ingest_aiwar_json(SAMPLE).unwrap();
for row in aiwar_node_rows(&g) {
assert_eq!(row.key.classid(), NodeGuid::CLASSID_OSINT);
assert_eq!(row.key.classid(), expected_classid);
assert_eq!(row.value, [0u8; 480], "head-only: value slab stays zero");
}
}
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
29 changes: 26 additions & 3 deletions crates/lance-graph-contract/src/soa_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,20 @@ pub struct DomainSpec {
pub member_edge: &'static str,
}

/// The **OSINT / Palantir-Gotham** domain (classid [`NodeGuid::CLASSID_OSINT`]):
/// The OSINT domain classid: the V3 OSINT class (`CLASSID_OSINT_V3`) in a
/// normal build; the legacy V1 `CLASSID_OSINT` only under
/// `--no-default-features` (where the V3 class is gated out). aiwar mints under
/// the same selector, so its rows always match this domain's projector filter.
#[cfg(feature = "guid-v3-tail")]
const OSINT_GOTHAM_CLASSID: u32 = NodeGuid::CLASSID_OSINT_V3;
#[cfg(not(feature = "guid-v3-tail"))]
const OSINT_GOTHAM_CLASSID: u32 = NodeGuid::CLASSID_OSINT;

/// The **OSINT / Palantir-Gotham** domain (V3 class [`NodeGuid::CLASSID_OSINT_V3`]):
/// a neo4j-emulation entity graph. Anchor families are caller-supplied (the key
/// entities of an investigation); the default declares none.
pub const OSINT_GOTHAM: DomainSpec = DomainSpec {
classid: NodeGuid::CLASSID_OSINT,
classid: OSINT_GOTHAM_CLASSID,
name: "OSINT/Gotham",
anchor_families: &[],
in_family_edge: "linked",
Expand Down Expand Up @@ -401,6 +410,7 @@ mod tests {
in_fam: &[u8],
out_fam: &[u8],
) -> NodeRow {
use crate::canonical_node::classid_read_mode;
let mut edges = EdgeBlock::default();
for (i, &b) in in_fam.iter().enumerate().take(12) {
edges.in_family[i] = b;
Expand All @@ -409,7 +419,20 @@ mod tests {
edges.out_family[i] = b;
}
NodeRow {
key: NodeGuid::new(domain.classid, hht.0, hht.1, hht.2, family, identity),
// Route through `mint_for` so the domain classid's `tail_variant`
// drives the tail layout (V3 domains lay family/identity as u16 at
// 12..16; V1 as u24 at 10..16) — matching how `family_of`/`identity_of`
// decode them (ISS-V1-TAIL-RESIDUE).
key: NodeGuid::mint_for(
classid_read_mode(domain.classid).tail_variant,
domain.classid,
hht.0,
hht.1,
hht.2,
0,
family,
identity,
),
edges,
value: [0u8; 480],
}
Expand Down
Loading