diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index e412c2da..024091ed 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,5 +1,34 @@ # LATEST_STATE — What Just Shipped (read this FIRST) +## 2026-07-17 — branch `claude/review-claude-board-files-nhqgx1` — D-TRI-1 value-tenant half: the autopoiesis triangle lands as 3 SoA lanes + +**IN PR.** The value-tenant half of the D-TRI-1 batched mint +(`triangle-tenants-gestalt-separation-v1.md` §1). Three NEW append-only +`ValueTenant`s — `FrozenStyle = 10`, `LearnedStyle = 11`, `ExploreStyle = 12` +— each `ColumnKind::U8 × 12` (12 palette256 atoms), contiguous at `row_offset` +152 / 164 / 176 (value-slab `[120,156)`), appended **after Kanban** per the +2026-07-17 operator ruling ("triangle right after the kanban board"). +**Additive, reserve-don't-reclaim, layout-preserving** (Full carve 120→156 B, +`NODE_ROW_STRIDE` 512 untouched, no `ENVELOPE_LAYOUT_VERSION` bump). Added to +`ValueSchema::Full` **only** — deliberately NOT `Cognitive`, so entity classes +(OSINT/PROJECT/ERP/Commerce, which resolve to Cognitive) do **not** inherit a +thinking-style triangle; the thinking-row schema decision is deferred to P4 +(ancestry pipeline). Slot `f` = `StyleFamily` ordinal 0..11 **or** compiled- +template step 0..11 (one content-blind register, ClassView-selected reading, +plan §4). **Atom 0 = null default** (zero-fallback: an un-populated lane reads +all-null, never a wrong policy). Accessors `NodeRow::{style_lane, set_style_lane, +triangle_for}` (`triangle_for(f) -> (frozen[f], learned[f], explore[f])`, the +one-glance per-family read the dispatch/perturbation/learning ancestry pipeline +resolves against; `set_style_lane` is the owner-`&mut` write that resolves the +W4b orphan-write flag). **The I-LEGACY field-isolation matrix** +(`thinking_style_triangle_tenant_carve_field_isolation_matrix`) certifies each +lane's 12 bytes flip in isolation, key/edges/other-tenant untouched. +**906 contract lib tests green; clippy `-D warnings` + fmt clean.** +**Still queued (the classid half — cross-repo OGAR mint):** chess domain 0x06 +concepts, the Tasks-SoA task-row classid (cognitive-task concepts), and +BoardAggregates @ row_offset 188 — one batched OGAR-originated mint (never solo). +Refs: STATUS_BOARD D-TRI-1, plan §1/§5/§6, v3-envelope-auditor gate. + ## 2026-07-16 — branch `claude/x265-x266-plans-review-h9osnl` (v5, post-#702) — probe-wave verdicts on main #702 MERGED (autonomous merge under the operator-granted authority): diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index a7726ead..62661dbd 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,7 +4,7 @@ Plan: `.claude/plans/triangle-tenants-gestalt-separation-v1.md`. Design shipped; | D-id | Title | Repo | Status | Evidence | |---|---|---|---|---| -| D-TRI-1 | Triangle tenant spec (Frozen/Learned/Explore, 12 slots x palette256) through envelope-auditor T1-T6, behind the batched mint | lance-graph | Queued (LAYOUT-GATED) | plan §1, §6 | +| D-TRI-1 | Triangle tenant spec (Frozen/Learned/Explore, 12 slots x palette256) through envelope-auditor T1-T6, behind the batched mint | lance-graph | **VALUE-TENANT HALF IN PR** — 3 lanes `ValueTenant::{FrozenStyle=10,LearnedStyle=11,ExploreStyle=12}` U8×12 at row_offset 152/164/176 (value-slab [120,156)), additive after Kanban per operator ruling; in `Full` only (NOT Cognitive — entity classes keep their carve), zero ENVELOPE_LAYOUT_VERSION bump; `NodeRow::{style_lane,set_style_lane,triangle_for}` + field-isolation matrix test; v3-envelope-auditor gated. **classid half (chess 0x06 + Tasks-SoA + BoardAggregates @188) still queued — the cross-repo OGAR mint.** | plan §1, §6 | | D-TRI-2 | 12-family vs 12-step reading agreement: jc battery (ICC, Pearson/Spearman, Cronbach alpha) over real shader cycles; collapse only on measured identity | lance-graph | Queued | plan §4, §6 | | D-TRI-3 | Nail->hammer dispatch probe: object resonance -> atom retrieval vs inverted baseline; no inverted read path exists structurally | lance-graph | Queued | plan §3, §6 | | D-TRI-4 | Chess<->thinking transfer measurement (validity + reliability) — the quarantine-lift gate | lance-graph + stockfish-rs | Queued | plan §5, §6 | diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index d615fd15..604c7eed 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -1,5 +1,14 @@ # Technical Debt Log — Open + Paid (double-entry, append-only) +## TD-TRI-1-P4-OBLIGATIONS — two forward obligations the v3-envelope-auditor flagged on the D-TRI-1 value-tenant half (2026-07-17, PR #717) + +**Surfaced by the v3-envelope-auditor** on the LAYOUT-CLEAN verdict for the triangle value tenants (`FrozenStyle`/`LearnedStyle`/`ExploreStyle`, PR #717). Neither gates #717 (the layout is additive + byte-isolated); both are **P4 (ancestry-pipeline) obligations** that must land WITH the reader that decodes these lanes: + +1. **The 226-atom cognitive palette256 codebook MUST reserve index 0 as the null / no-op atom.** The triangle's zero-fallback (`atom 0 = null default`, `NodeRow::{style_lane,triangle_for}`) is byte-safe today (reserved tail zero-fills, no persisted corpus wrote `[152,188)`), but the *reading* semantics are unbacked: that codebook does not exist in code yet (grep finds only the plan doc + the new doc-comments). When it lands, a zeroed slot must decode to "no policy," NOT "verb 0" (a live, wrong policy). Recommend a codebook-level `assert!`/test reserving index 0. This is the le-contract §3b split (bytes-don't-move is proven; the reading is a jc-pillar obligation). +2. **The P4 thinking-row `ValueSchema` MUST include `FrozenStyle`/`LearnedStyle`/`ExploreStyle`** — or thinking rows must resolve to `Full`. The triangle is in `Full` ONLY (not `Cognitive`, deliberately — entity classes OSINT/PROJECT/ERP keep their carve). Reads are schema-independent (accessors read raw slab bytes), so no read-side gap today; the risk is *persist-side*: a thinking row with POPULATED triangle data that resolves to `Cognitive` would have those bytes dropped on a `field_mask`-driven bake. Today `DEFAULT → Full` covers unmapped rows. + +Both recorded so P4 (D-TRI-2/3 ancestry-pipeline consolidation) inherits them. Refs: PR #717, plan §1/§4, `E-*` (P4 wiring session to append the reader-side finding). + ## TD-PLANNER-STYLE-DEFAULT-DRIFT-1 — PAID (2026-07-17, same day as surfaced) **Paid** in `crates/lance-graph-planner/src/thinking/style.rs` `default_modulation`. The 5 formerly-`_ => default()` families (`Convergent`/`Systematic`/`Divergent`/`Diffuse`/`Peripheral`) now carry the canonical `UNIFIED_STYLES`≡`StyleParams` values on the three MEASURED dims: `Convergent` 0.75/4/0.10, `Systematic` 0.70/5/0.10, `Divergent` 0.40/10/0.70, `Diffuse` 0.45/8/0.40, `Peripheral` 0.20/20/0.60. The **4 planner-specific dims** (`depth_bias`/`breadth_bias`/`noise_tolerance`/`speed_bias`) have NO canonical source, so they stay at `FieldModulation::default()` via `..default()` struct-update — filled only what was measured, fabricated nothing. The `_` fallback is REMOVED → the match is exhaustive over all 12 `StyleFamily` variants (a future variant is now a compile error, not a silent default). Regression test `formerly_defaulted_families_match_canonical_on_measured_dims` pins the 5 families' 3 dims + asserts the 4 unmeasured dims stayed at default; `every_family_has_a_nondefault_or_explicit_modulation` pins exhaustiveness. 218 planner lib tests green (+2), clippy `-D warnings` exit 0. Behavior change: the 5 families now search with their real per-family resonance/fan_out/exploration instead of the flat default — a correctness improvement (the drift D-TSC-1b measured is retired; re-running the probe would now show Mode A ≈ Mode B). Numeric half of census question O5 answered. See `E-PLANNER-STYLE-DRIFT-PAID-1`. diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 04b9271d..c3dfb0ef 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -853,6 +853,28 @@ pub enum ValueTenant { /// Pins SoA↔kanban in the LE blob at value-slab `[112,120)` (subsumes the /// envelope-pointer G1: the node carries its own phase + cycle). Kanban = 9, + /// **Autopoiesis-triangle FROZEN lane** — 12 palette256 atoms (12 B, one per + /// [`StyleFamily`](crate::style_family::StyleFamily) ordinal 0..11, OR per + /// compiled-template step 0..11; one content-blind register, the reading is + /// ClassView-selected). The row's CHECKPOINT policy — the atom the + /// can't-stop-thinking dispatch runs off, no lookup beyond the row. **Atom 0 + /// = null default** (zero-fallback: an un-populated lane reads all-null, never + /// a wrong policy — additive lane, reserve-don't-reclaim). Appended after + /// `Kanban` per the 2026-07-17 operator ruling ("triangle right after the + /// kanban board"); triangle plan `.claude/plans/triangle-tenants-gestalt-separation-v1.md` §1. + FrozenStyle = 10, + /// **Autopoiesis-triangle LEARNED lane** — same 12-slot palette256 shape; the + /// NARS-revision-updated policy the L4 learning seam writes (owner `&mut` on + /// the row's own lane, `l4_bridge` outcome-driven — resolves the W4b + /// ORPHAN-WRITE flag). `learned[f]` promotes to `frozen[f]` only after winning + /// the held-out arm (a kanbanstep-interior act, no events). + LearnedStyle = 11, + /// **Autopoiesis-triangle EXPLORE lane** — same 12-slot palette256 shape; the + /// exploration variant fed by the P64 perturbation ladder + /// (`StreamDto → PerturbationDto`). Deterministic address-derived jitter (the + /// D-QUANTGATE coprime walk — never RNG, replay holds), so a re-run yields the + /// identical explore atom for the same address. + ExploreStyle = 12, } impl ValueTenant { @@ -938,12 +960,36 @@ pub const VALUE_TENANTS: &[ColumnDescriptor] = &[ }, ColumnDescriptor { // kanban×Rubicon cursor: 8 B contiguous at row_offset 144 (value-slab - // [112,120)); reserve-don't-reclaim, layout-preserving (Full ends 152 ≤ 480). + // [112,120)); reserve-don't-reclaim, layout-preserving (Kanban ends 152 ≤ 480). name_id: ValueTenant::Kanban as u16, kind: ColumnKind::U64, elems_per_row: 1, row_offset: 144, }, + // ── Autopoiesis triangle: three 12-byte palette256-atom lanes, appended after + // Kanban per the operator ruling. 3 × 12 = 36 B contiguous [152,188) + // (value-slab [120,156)); additive, reserve-don't-reclaim, layout-preserving + // (Full now ends 188 ≤ 480, NODE_ROW_STRIDE unchanged → no + // ENVELOPE_LAYOUT_VERSION bump). BoardAggregates (W2a) reserves row_offset + // 188 next in the same batched mint. + ColumnDescriptor { + name_id: ValueTenant::FrozenStyle as u16, + kind: ColumnKind::U8, + elems_per_row: 12, + row_offset: 152, + }, + ColumnDescriptor { + name_id: ValueTenant::LearnedStyle as u16, + kind: ColumnKind::U8, + elems_per_row: 12, + row_offset: 164, + }, + ColumnDescriptor { + name_id: ValueTenant::ExploreStyle as u16, + kind: ColumnKind::U8, + elems_per_row: 12, + row_offset: 176, + }, ]; // Compile-time canon: VALUE_TENANTS is discriminant-ordered, contiguous within the @@ -1033,6 +1079,15 @@ impl ValueSchema { ValueTenant::Plasticity as u8, ValueTenant::EntityType as u8, ValueTenant::Kanban as u8, + // Autopoiesis triangle — Full is the densest node, so it carries + // every tenant (the `Full covers every tenant` compile assert). The + // thinking-row schema that materialises the triangle for hot + // dispatch is P4's (ancestry-pipeline) call; Cognitive is left + // unchanged so entity classes (OSINT/PROJECT/ERP …) do not inherit + // a style triangle they have no use for. + ValueTenant::FrozenStyle as u8, + ValueTenant::LearnedStyle as u8, + ValueTenant::ExploreStyle as u8, ]), } } @@ -1589,6 +1644,70 @@ impl NodeRow { let gate = crate::mul::i4_eval::gate_decision_i4(&self.qualia(), mantissa); self.kanban().phase.advance_on_gate(&gate) } + + /// Read a 12-byte thinking-style triangle lane — the raw palette256 atoms, + /// zero-copy decode. `tenant` MUST be one of [`ValueTenant::FrozenStyle`] / + /// [`LearnedStyle`](ValueTenant::LearnedStyle) / + /// [`ExploreStyle`](ValueTenant::ExploreStyle) (`byte_len() == 12`, + /// debug-asserted). Slot `f` is the [`StyleFamily`](crate::style_family::StyleFamily) + /// ordinal 0..11 (or the compiled-template step, ClassView-selected). Atom 0 = + /// null default, so an un-populated lane reads all-zero. **Pure read** (no + /// `&mut` during compute, per the borrow-strategy rule). + #[inline] + #[must_use] + pub fn style_lane(&self, tenant: ValueTenant) -> [u8; 12] { + // `tenant` MUST be a 12-byte triangle lane; any other tenant returns the + // null lane (a non-12-byte tenant would otherwise read past its lane into + // the next tenant's bytes — release-safe by construction, not a + // debug-only guard). + if tenant.byte_len() != 12 { + return [0u8; 12]; + } + let o = tenant.value_offset(); + let mut b = [0u8; 12]; + b.copy_from_slice(&self.value[o..o + 12]); + b + } + + /// Write a 12-byte thinking-style triangle lane. **Owner-only by convention** + /// (the `MailboxSoaOwner` learning/promotion path — the write lands on the + /// row's own lane, resolving the W4b orphan-write flag); reads stay zero-copy. + /// Bumps the per-tenant update counter (no-op unless `tenant-counters`). + #[inline] + pub fn set_style_lane(&mut self, tenant: ValueTenant, atoms: [u8; 12]) { + // `tenant` MUST be a 12-byte triangle lane; for any other tenant this is + // a no-op (stamping 12 bytes onto a shorter tenant would corrupt the + // adjacent tenants — release-safe by construction). + if tenant.byte_len() != 12 { + return; + } + let o = tenant.value_offset(); + self.value[o..o + 12].copy_from_slice(&atoms); + crate::tenant_counter::tenant_update(tenant); + } + + /// The autopoiesis triangle for one style family: `(frozen, learned, explore)` + /// palette256 atoms — three bytes, one glance, zero decode (triangle plan §1 + /// per-family read). `family_ordinal` is the [`StyleFamily`](crate::style_family::StyleFamily) + /// ordinal 0..11 (values ≥ 12 read the reserved slots). This is the read the + /// dispatch/perturbation/learning ancestry pipeline resolves against. + #[inline] + #[must_use] + pub fn triangle_for(&self, family_ordinal: u8) -> (u8, u8, u8) { + // Out-of-range slots (≥ 12 — a malformed template step or a future + // StyleFamily ordinal) return the null triangle (atom 0 everywhere) — + // the zero-fallback "never a wrong policy" contract. Without this guard + // the debug_assert vanishes in release and the index runs past each + // 12-byte lane, aliasing the next tenant's bytes (Codex P2 on #717). + if family_ordinal >= 12 { + return (0, 0, 0); + } + let i = family_ordinal as usize; + let fo = ValueTenant::FrozenStyle.value_offset(); + let lo = ValueTenant::LearnedStyle.value_offset(); + let eo = ValueTenant::ExploreStyle.value_offset(); + (self.value[fo + i], self.value[lo + i], self.value[eo + i]) + } } #[cfg(test)] @@ -2093,8 +2212,8 @@ mod tests { assert!(prev_end <= NODE_ROW_STRIDE); assert_eq!( prev_end - VALUE_SLAB_ROW_OFFSET, - 120, - "current Full carve uses 120 of 480 B (helix 6 + kanban×Rubicon tenant 8)" + 156, + "current Full carve uses 156 of 480 B (kanban×Rubicon 8 + autopoiesis triangle 3×12=36)" ); assert!(prev_end - VALUE_SLAB_ROW_OFFSET <= VALUE_SLAB_LEN); } @@ -2129,11 +2248,12 @@ mod tests { #[test] fn value_schema_byte_budgets_are_locked() { assert_eq!(ValueSchema::Bootstrap.tenant_bytes(), 0); - // Cognitive 58 + Kanban 8 = 66; Full 112 + Kanban 8 = 120 (kanban×Rubicon - // tenant added — reserve-don't-reclaim, still ≤ 480, stride unchanged). + // Cognitive 58 + Kanban 8 = 66 (triangle NOT in Cognitive — entity classes + // keep their carve); Full 120 + 3×12 triangle = 156 (autopoiesis triangle + // added — reserve-don't-reclaim, still ≤ 480, stride unchanged). assert_eq!(ValueSchema::Cognitive.tenant_bytes(), 66); assert_eq!(ValueSchema::Compressed.tenant_bytes(), 56); - assert_eq!(ValueSchema::Full.tenant_bytes(), 120); + assert_eq!(ValueSchema::Full.tenant_bytes(), 156); for s in [ ValueSchema::Bootstrap, ValueSchema::Cognitive, @@ -2241,7 +2361,7 @@ mod tests { #[test] fn default_class_node_materialises_full_slab() { // End-to-end connect: a bootstrap NodeRow → its classid resolves to Full → - // the Full preset covers every tenant and uses the locked 120-byte carve. + // the Full preset covers every tenant and uses the locked 156-byte carve. let row = sample_row(NodeGuid::CLASSID_DEFAULT, 0x00_00CD); let rm = row.key.read_mode(); assert_eq!(rm.value_schema, ValueSchema::Full); @@ -2250,12 +2370,125 @@ mod tests { VALUE_TENANTS.len(), "Full read-mode materialises every value tenant" ); - assert_eq!(rm.value_schema.tenant_bytes(), 120); - // The slab has room (120 ≤ 480) and the choice never grows the stride. + assert_eq!(rm.value_schema.tenant_bytes(), 156); + // The slab has room (156 ≤ 480) and the choice never grows the stride. assert!(rm.value_schema.tenant_bytes() <= VALUE_SLAB_LEN); assert!(rm.is_layout_preserving()); } + #[test] + fn thinking_style_triangle_tenant_carve_field_isolation_matrix() { + // The autopoiesis triangle — three 12-byte palette256 lanes appended after + // Kanban per the 2026-07-17 operator ruling (plan §1). Certifies: (1) the + // contiguous carve [152,188); (2) Full carries it, entity carves do NOT; + // (3) the I-LEGACY field-isolation matrix; (4) typed-accessor round-trip + + // atom-0 zero-fallback; (5) key/edges/other-tenant isolation. + use ValueTenant::{ExploreStyle, FrozenStyle, LearnedStyle}; + + // (1) Contiguous, 12 B each, right after Kanban (ends 152); triangle ends + // 188 ≤ 480, so BoardAggregates (W2a) reserves row_offset 188 next. + assert_eq!(FrozenStyle.value_offset(), 152 - VALUE_SLAB_ROW_OFFSET); + assert_eq!(LearnedStyle.value_offset(), 164 - VALUE_SLAB_ROW_OFFSET); + assert_eq!(ExploreStyle.value_offset(), 176 - VALUE_SLAB_ROW_OFFSET); + for t in [FrozenStyle, LearnedStyle, ExploreStyle] { + assert_eq!(t.byte_len(), 12, "{t:?} lane is 12 palette256 atoms"); + } + assert_eq!( + ExploreStyle.value_offset() + 12, + 188 - VALUE_SLAB_ROW_OFFSET + ); + + // (2) Full carries the triangle (densest node); Cognitive/Compressed do + // NOT — entity classes (OSINT/PROJECT/ERP …) keep their carve. + for t in [FrozenStyle, LearnedStyle, ExploreStyle] { + assert!(ValueSchema::Full.has(t), "Full materialises {t:?}"); + assert!( + !ValueSchema::Cognitive.has(t), + "Cognitive must NOT carry {t:?}" + ); + assert!( + !ValueSchema::Compressed.has(t), + "Compressed must NOT carry {t:?}" + ); + } + + // (3) THE MATRIX: flip each lane; assert only its 12 bytes changed. Uses + // the shared carve-order snapshot logic (no restore between lanes — + // each lane checks against its own pre-flip snapshot). + let mut row = sample_row(NodeGuid::CLASSID_DEFAULT, 0x00_00CD); + let key_before = row.key; + let edges_before = row.edges; + for t in [FrozenStyle, LearnedStyle, ExploreStyle] { + let before = row.value; + let (o, n) = (t.value_offset(), t.byte_len()); + for b in &mut row.value[o..o + n] { + *b ^= 0xFF; + } + for (i, (&now, &was)) in row.value.iter().zip(before.iter()).enumerate() { + if (o..o + n).contains(&i) { + assert_ne!(now, was, "{t:?} lane byte {i} must have flipped"); + } else { + assert_eq!(now, was, "byte {i} outside {t:?} lane must not change"); + } + } + } + assert_eq!(row.key, key_before, "key untouched by triangle writes"); + assert_eq!( + row.edges, edges_before, + "edges untouched by triangle writes" + ); + + // (4) Typed accessors round-trip; atom 0 = null default (zero-fallback). + let mut row = sample_row(NodeGuid::CLASSID_DEFAULT, 0x00_00CD); + assert_eq!( + row.style_lane(FrozenStyle), + [0u8; 12], + "un-populated lane reads all-null" + ); + assert_eq!( + row.triangle_for(3), + (0, 0, 0), + "family-3 triangle null by default" + ); + let frozen: [u8; 12] = core::array::from_fn(|i| (i as u8) + 1); + let learned: [u8; 12] = core::array::from_fn(|i| (i as u8) + 20); + let explore: [u8; 12] = core::array::from_fn(|i| (i as u8) + 40); + row.set_style_lane(FrozenStyle, frozen); + row.set_style_lane(LearnedStyle, learned); + row.set_style_lane(ExploreStyle, explore); + assert_eq!(row.style_lane(FrozenStyle), frozen); + assert_eq!(row.style_lane(LearnedStyle), learned); + assert_eq!(row.style_lane(ExploreStyle), explore); + // Per-family read: (frozen[f], learned[f], explore[f]) — one glance. + assert_eq!(row.triangle_for(0), (1, 20, 40)); + assert_eq!(row.triangle_for(11), (12, 31, 51)); + // Out-of-range ordinal returns the null triangle — never a wrong-lane + // atom, even with the lanes populated (Codex P2 regression: slot 12 + // would otherwise alias LearnedStyle[0]/ExploreStyle[0]/reserved). + assert_eq!(row.triangle_for(12), (0, 0, 0)); + assert_eq!(row.triangle_for(255), (0, 0, 0)); + // A non-triangle tenant is release-safe: null read, no-op write. + assert_eq!(row.style_lane(ValueTenant::EntityType), [0u8; 12]); + let snapshot = row.value; + row.set_style_lane(ValueTenant::EntityType, [9u8; 12]); + assert_eq!( + row.value, snapshot, + "set_style_lane on a non-triangle tenant is a no-op" + ); + + // (5) Writing the triangle never disturbs an adjacent tenant (Kanban) or + // the key/edges. + assert_eq!( + row.kanban(), + KanbanTenant::default(), + "kanban lane untouched" + ); + assert_eq!( + row.key, key_before, + "key untouched by triangle accessor writes" + ); + } + #[test] fn osint_and_fma_classids_resolve_to_their_read_modes() { // The two registered graph domains (see `soa_graph`): OSINT/Gotham is a