Skip to content

Commit 85963fe

Browse files
committed
feat(hhtl): signal NiblePath depth-exhaustion (is_full/try_child) + record #442 scale-freezes (D-ARM-14 review)
The D-ARM-14 session's review of #442 (merge-worthy verdict) flagged two non-blocking 'name the freeze' items; acting on both: Flag 1 (the strong one) — NiblePath::child() saturated SILENTLY past MAX_DEPTH=16, so two distinct deep P279 chains truncated at 16 would collide on one path (is_ancestor_of/basin treat them as one). Same no-silent-aliasing class as the root()/FieldMask fixes. Added is_full() (the depth_exhausted flag) + try_child() -> Option (returns None instead of saturating), so the deferred 115M loader DETECTS the ceiling and switches to a ref instead of colliding. child() stays the saturating convenience, now documented to gate on is_full(). Flag 2 + minors — recorded as TD-WIKI-SCALE (TECH_DEBT, append-only): StructuralSignature u32 birthday ceiling (~77k shape-families — a #441 contract decision to widen to u64, WITH the deferred loader, not unilaterally here); signature() per-call Vec alloc; dolce_category_id default-ENDURANT-on-unknown. All fine at curated/Odoo scale; bite only at the 115M load. signature() doc now cites the u32 freeze. 503 contract + 246 ontology lib green; clippy -D warnings + fmt clean. Firewall preserved; the proposer-side dolce_id alignment remains the D-ARM-14 session's lane. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R
1 parent 5ad7c68 commit 85963fe

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

.claude/board/TECH_DEBT.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,32 @@
1313
---
1414

1515

16+
### TD-WIKI-SCALE (D-WIKI-HHTL / #442) — three scale-freezes the N4 falsifier inherits
17+
18+
**Status: Open.** Surfaced by the D-ARM-14 session's review of #442 (the hub-side
19+
Wikidata-HHTL arc). All three are FINE at the curated-corpus + Odoo scale this PR
20+
ships; they bite only at the deferred 115M streaming load:
21+
22+
1. **`StructuralSignature` u32 birthday ceiling (~77k).** `wikidata_hhtl::WikidataClass::signature()`
23+
and Odoo's `class_signature` both return `StructuralSignature(u32)` (#441's type). ~50% collision
24+
probability near ~77k distinct shape-families → two genuinely different shapes alias to one family
25+
(a correctness MERGE, not a perf nit) at Wikidata scale. **Pay by:** widen `StructuralSignature`
26+
to `u64` — a #441 contract decision (touches the Odoo signature path too); land it WITH the
27+
deferred load slice, not unilaterally in #442.
28+
2. **`NiblePath` MAX_DEPTH=16 ceiling — now SIGNALED, not silent.** `child()` saturates silently past
29+
depth 16; real P279 chains run deeper, so two distinct deep classes truncated at 16 would collide on
30+
one path. #442 adds `is_full()` + `try_child() -> Option` so the deferred loader DETECTS the ceiling
31+
and switches to a ref (the bit-budget escape) instead of colliding. **Pay by:** the deferred loader
32+
gates every descent on `is_full()`/`try_child()`; the ref-escape store is its own slice.
33+
3. **`signature()` allocates+sorts a `Vec` per call; `dolce_category_id()` defaults `ENDURANT` on
34+
unknown class_id** (silent default vs signaling absence — mirrors `RegistryClassView`). Both fine
35+
for the fixture. **Pay by:** at load scale hash the property-set without a per-call alloc
36+
(pre-sorted columnar input); decide whether unknown-class DOLCE should be `Option`-signaled (a #441
37+
`ClassView` trait decision).
38+
39+
Cross-ref: #442, #441 (StructuralSignature/ClassView), the D-ARM-14 review, `wikidata-hhtl-load.md`,
40+
FINDING D-CLS↔D-ARM-14 (EPIPHANIES).
41+
1642
### TD-ARM-CARRIER-FORK (D-ARM-13 / streaming-arm-nars-discovery-v1)
1743

1844
**Status: Open.** Surfaced by the 3-savant brutal review of D-ARM-13

crates/lance-graph-contract/src/hhtl.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,15 @@ impl NiblePath {
8080
}
8181
}
8282

83-
/// Route one level deeper to child `nibble`. Returns `self` UNCHANGED once
84-
/// [`MAX_DEPTH`] is reached (the path is full — deeper addressing needs a ref)
85-
/// or `nibble >= FAN_OUT` (out of range — never folded onto a valid child,
86-
/// mirroring [`FieldMask`](crate::class_view::FieldMask)'s out-of-range discipline).
83+
/// Route one level deeper to child `nibble`. **Saturating:** returns `self`
84+
/// UNCHANGED once [`MAX_DEPTH`] is reached or `nibble >= FAN_OUT` (out of range —
85+
/// never folded onto a valid child, mirroring
86+
/// [`FieldMask`](crate::class_view::FieldMask)'s out-of-range discipline).
87+
///
88+
/// At [`MAX_DEPTH`] the silent saturation means two *distinct* deeper paths would
89+
/// collide on this address — so a real-scale caller MUST gate on
90+
/// [`is_full`](NiblePath::is_full) or use [`try_child`](NiblePath::try_child),
91+
/// which signal the ceiling instead of colliding (D-ARM-14 review of #442).
8792
#[must_use]
8893
pub const fn child(self, nibble: u8) -> Self {
8994
if self.depth >= MAX_DEPTH || nibble >= FAN_OUT {
@@ -96,6 +101,33 @@ impl NiblePath {
96101
}
97102
}
98103

104+
/// Has this path reached [`MAX_DEPTH`] — i.e. [`child`](NiblePath::child) can no
105+
/// longer descend within the `u64`? When `true`, the bit-budget discipline
106+
/// (`wikidata-hhtl-load.md`:71 "grows unbounded → path/ref") says switch to a
107+
/// ref for deeper addressing: descending anyway via [`child`] is a SILENT no-op,
108+
/// so two distinct deeper classes would collide on this same path. The deferred
109+
/// 115M loader gates each descent on this (D-ARM-14 review of #442).
110+
#[must_use]
111+
pub const fn is_full(self) -> bool {
112+
self.depth >= MAX_DEPTH
113+
}
114+
115+
/// Route one level deeper, returning `None` instead of silently saturating when
116+
/// the path [`is_full`](NiblePath::is_full) or `nibble >= FAN_OUT`. The explicit
117+
/// counterpart to [`child`](NiblePath::child) for callers that must NOT collide
118+
/// distinct deep paths (the real-scale loader).
119+
#[must_use]
120+
pub const fn try_child(self, nibble: u8) -> Option<Self> {
121+
if self.depth >= MAX_DEPTH || nibble >= FAN_OUT {
122+
None
123+
} else {
124+
Some(Self {
125+
path: (self.path << 4) | (nibble as u64),
126+
depth: self.depth + 1,
127+
})
128+
}
129+
}
130+
99131
/// The basin (root) nibble — the DOLCE top facet this path lives under.
100132
/// `None` for the empty path.
101133
#[must_use]
@@ -262,4 +294,33 @@ mod tests {
262294
);
263295
assert_eq!(bat_mask.count(), 4);
264296
}
297+
298+
#[test]
299+
fn is_full_and_try_child_signal_depth_exhaustion() {
300+
// child() saturates silently at MAX_DEPTH; is_full()/try_child() expose the
301+
// ceiling so the deferred loader switches to a ref instead of colliding two
302+
// distinct deep paths (D-ARM-14 review of #442).
303+
let mut p = NiblePath::root(0x1);
304+
assert!(!p.is_full());
305+
while !p.is_full() {
306+
p = p.try_child(0xF).expect("descends while not full");
307+
}
308+
assert_eq!(p.depth(), MAX_DEPTH);
309+
assert!(p.is_full());
310+
assert_eq!(
311+
p.try_child(0x2),
312+
None,
313+
"try_child signals exhaustion, not a silent collision"
314+
);
315+
assert_eq!(
316+
p.child(0x2),
317+
p,
318+
"child() still saturates (the convenience path)"
319+
);
320+
assert_eq!(
321+
NiblePath::root(0x1).try_child(16),
322+
None,
323+
"out-of-range nibble is None too"
324+
);
325+
}
265326
}

crates/lance-graph-ontology/src/wikidata_hhtl.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ impl WikidataClass {
9292
/// family (classes.md:42, now on Wikidata). Reuses the canonical
9393
/// [`fnv1a`](lance_graph_contract::hash::fnv1a); truncated to the
9494
/// [`StructuralSignature`] `u32`.
95+
///
96+
/// **Scale freeze (TD-WIKI-SCALE):** `StructuralSignature` is `u32` — ~50%
97+
/// birthday-collision near ~77k distinct shape-families. Safe for the curated
98+
/// corpus + Odoo; at full Wikidata load scale, widening to `u64` is a #441
99+
/// contract decision (flagged by the D-ARM-14 review of #442), to land WITH the
100+
/// deferred loader, not unilaterally here.
95101
#[must_use]
96102
pub fn signature(&self) -> StructuralSignature {
97103
let mut props: Vec<&str> = self.properties.to_vec();

0 commit comments

Comments
 (0)