Skip to content

H0 cache invariant + H1/H2 inheritance fixes#10

Merged
david-w-t merged 7 commits intodavidwt-com:mainfrom
david-w-t:develop
May 8, 2026
Merged

H0 cache invariant + H1/H2 inheritance fixes#10
david-w-t merged 7 commits intodavidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t
Copy link
Copy Markdown
Contributor

Summary

Lands the H0 series — establishes the "arcs authoritative; hierarchy lists cached" invariant — plus two H-severity inheritance fixes (H1, H2) and the close-out of TASKS-CRITICAL.md.

The kernel now treats relationships as the sole authoritative source for taxonomy / composition / instantiation. node.parents and node.classes are caches; their consistency is asserted by graphdb_mgr:verify_caches/0 after every CT testcase and at bootstrap completion.

Commits (7)

Commit Summary
33bc848 Retire TASKS-CRITICAL.md and clean up doc references (next-series opener)
a078373 H1+H2: resolve_from_class walks class taxonomy; P4 filters to kind=connection
d5a7244 H0a: charter arcs-authoritative.md and the H0 substep checklist
0b5fc43 H0b: retire node.parent; add parents/classes cache lists; arc-based downward lookups
ce07cb2 H0c: graphdb_mgr:verify_caches/0 + rebuild_caches/0; CT enforcement
9e5d64a H0d: bootstrap.terms to Option B (5-tuple node form); loader rebuilds + verifies
f2fead8 H0e: fold cache invariant into ARCHITECTURE.md §3; mark H0+M1 RESOLVED

Schema change

%% Before                                  %% After
-record(node, {                            -record(node, {
  nref,                                      nref,
  kind,                                      kind,
  parent,        %% integer() | undefined    parents = [],   %% [integer()] cache
  attribute_value_pairs                      classes = [],   %% [integer()] cache
}).                                          attribute_value_pairs
                                           }).

The `{index, [parent]}` secondary index on `nodes` is retired; downward queries ("children of X") read outgoing arcs from `relationships` filtered by kind + characterization.

Closes

  • H0 in TASKS-HIGH.md (was the active front)
  • H1, H2 in TASKS-HIGH.md (inheritance correctness)
  • M1 in TASKS-MEDIUM.md (PART-OF dual-storage decision)
  • M2 in TASKS-MEDIUM.md (subsumed by H1)

Test plan

  • `./rebar3 compile` clean (zero warnings)
  • `./rebar3 ct` — 122 Common Test cases passing
  • `./rebar3 eunit` — 64 EUnit cases passing
  • Every CT `end_per_testcase` runs `graphdb_mgr:verify_caches/0` and asserts `ok`
  • Bootstrap loader runs `rebuild_caches/0` + `verify_caches/0`; throws on mismatch
  • Direct CT coverage for verify/rebuild (`graphdb_mgr_SUITE` cache_audit group)

🤖 Generated with Claude Code

david-w-t and others added 7 commits May 6, 2026 21:25
Opens the next series of work. C1, C2, and C3 landed in PR davidwt-com#9
(`ce2e281`); the task file's job is done. Per the task-file retirement
workflow, this commit removes the file and updates every doc that
enumerated it: project root `README.md`, `CLAUDE.md`, `ARCHITECTURE.md`,
plus `apps/database/CLAUDE.md` and `apps/graphdb/CLAUDE.md`. The
remaining task files are now `TASKS-HIGH.md`, `TASKS-MEDIUM.md`, and
`TASKS-LOW.md`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
H1 (graphdb_instance:resolve_from_class)
  Stop reading exactly one class node's AVPs.  Locate the membership
  arc via the existing do_class_of/1, then ask graphdb_class for the
  class node and ancestor chain and return the first AVP match.
  Subsumes M2 (resolve_from_class no longer reads the nodes table
  directly or hard-codes ?CLASS_MEMBERSHIP_ARC inside the resolver).

H2 (graphdb_instance:resolve_from_connected)
  Filter the outgoing relationships to kind = connection before
  pulling target nrefs.  Instantiation (membership, char=29) and
  composition (parent/child, char=27/28) arcs no longer feed
  Priority 4; those targets are already covered by Priorities 2 and 3.

Tests
  Three new CT cases in graphdb_instance_SUITE:
    - resolve_value_walks_class_taxonomy
    - resolve_value_local_class_overrides_taxonomy_ancestor
    - resolve_value_p4_ignores_compositional_arc
  Full suite: 118 CT + 64 EUnit = 182, all green.

Docs
  TASKS-HIGH.md: H1 and H2 marked RESOLVED with status notes; intro
  rewritten to reflect that only H3-H5 remain.
  TASKS-MEDIUM.md: M2 marked RESOLVED (closed by H1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the H0 decision record (arcs-authoritative.md) and an H0 entry
to TASKS-HIGH.md with substep checklist (H0a-H0e).  No code changes.

Decision in summary:
  - Arcs are the sole authoritative source for hierarchy (taxonomy,
    composition, instantiation).
  - node.parents :: [integer()] and node.classes :: [integer()] are
    caches reconstructable from the arcs.  node.parent (singular) is
    retired.
  - Each cache field has exactly one owner worker; arc and cache
    updates happen in one mnesia:transaction/1.
  - Bootstrap follows Option B: arcs only in the file, with %%
    comments per relationship; loader rebuilds and verifies caches
    after writing arcs.
  - Cache/arc disagreement is a fatal error.

H0 lands before H3 so the multi-parent class case (H3) and
multi-class instance case (H4) build on a single uniform pattern
rather than schema migrations tangled with semantic changes.
H0 closes M1 ("PART-OF stored in two places with no consistency
invariant") on completion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Schema change for the "arcs authoritative; hierarchy lists cached"
invariant (charter: arcs-authoritative.md, plan: TASKS-HIGH.md H0b).

Record change in `node`:
  - retire `parent :: integer() | undefined`
  - add `parents = [] :: [integer()]` -- cache of compositional /
    taxonomic parent arcs
  - add `classes = [] :: [integer()]` -- cache of instantiation arcs
    (instances only)

Bootstrap loader (graphdb_bootstrap):
  - term_to_node converts the legacy single Parent integer into
    parents=[Parent] (or [] for the Root node) and seeds classes=[]
  - drop {index, [parent]} from create_tables; downward lookups now
    go through the relationships table
  - bootstrap.terms format unchanged (Option B switch lands in H0d)

Write paths populate caches transactionally:
  - graphdb_attr:do_create_attribute / do_create_seed_attribute
  - graphdb_class:do_create_class (class + default template),
    do_write_template, do_create_seed_attribute
  - graphdb_instance:do_write_instance (parents + classes)

Read paths replace the retired parent index:
  - downward "list children" lookups switch to a private
    downward_children_by_arc/3 helper that reads outgoing arcs from
    the relationships table filtered by Kind + characterization
  - graphdb_attr: find_attribute_by_name, do_list_children (char=24)
  - graphdb_class: find_attribute_by_name, do_find_template_by_name,
    do_templates_for_class (char=26 composition), do_subclasses
    (char=26 taxonomy)
  - graphdb_instance: do_children (char=28 composition)

Single-chain ancestor walks read parents=[P|_] via head_parent/1
helper; multi-parent traversal arrives in H3.  validate_template_scope
and resolve_from_ancestors switch to the same helper.

Tests: 64 EUnit + 118 CT = 182 all green.  Caches populated as
length-1 lists; single-parent semantics preserved per H0b spec.
graphdb_bootstrap_SUITE:load_category_children rewritten to verify
children via composition arcs (the parent index is gone).
graphdb_bootstrap_tests element/_ position assertions updated for
the new {node, Nref, Kind, Parents, Classes, AVPs} tuple shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the cache-invariant audit/repair APIs spec'd in
arcs-authoritative.md and wires them into every CT suite.

graphdb_mgr exports two new APIs:
  - verify_caches/0 -> ok | {error, [{Nref, Field, Expected, Actual}]}
    Scans every node, compares parents/classes against the arcs.
    Order-insensitive; returns the full mismatch list when wrong.
  - rebuild_caches/0 -> ok | {error, term()}
    Rewrites every node's parents/classes from the arcs in one txn.
    Used as a repair tool and (after H0d) as the bootstrap loader's
    post-load tail.

Internal helpers:
  - expected_parents/1 reads outgoing arcs (kind in {composition,
    taxonomy}) whose characterization is one of the parent labels
    {21, 23, 25, 27} and returns the target nrefs.
  - expected_classes/1 reads outgoing instantiation arcs (char=29)
    and returns the target class nrefs (empty for non-instances).
  - verify_one/1 / rebuild_one/1 -- per-node primitives.

CT enforcement -- every suite's end_per_testcase now calls a local
verify_cache_invariant/1 helper that asserts verify_caches/0 returns
ok before teardown.  A failed verify is a fatal CT failure with
ct:pal'd Mismatches.  All 118 pre-existing testcases still pass with
the wiring in place: the H0b write paths populate caches correctly.

New direct CT coverage (graphdb_mgr_SUITE cache_audit group, +4):
  - verify_caches_clean_after_bootstrap
  - verify_caches_detects_poisoned_parents
    (poison node 6's parents -> [9999], expect mismatch tuple)
  - verify_caches_detects_poisoned_classes
    (poison node 7's classes -> [42], expect mismatch tuple)
  - rebuild_caches_restores_after_poison
    (3 caches poisoned, rebuild, then verify ok)

Tests: 122 CT (+4) + 64 EUnit = 186 all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Arcs become the sole authoritative source of hierarchy in the
bootstrap file.  Node tuples no longer carry a Parent integer; the
existing inline %% arc comments make the file human-followable
top-to-bottom.

apps/graphdb/priv/bootstrap.terms:
  Old: {node, Nref, Kind, Parent, {NameAttr, Name}, AVPs}
  New: {node, Nref, Kind, {NameAttr, Name}, AVPs}
  All 31 node tuples converted; 30 relationship terms unchanged;
  documentation comments updated to describe the new shape.

graphdb_bootstrap.erl:
  - classify_terms/4, sort_nodes_by_kind/1, validate/2 match the
    5-tuple form.
  - term_to_node/1 sets parents=[], classes=[] (caches are filled
    by rebuild_caches/0 once all arcs are written).
  - do_load/0 calls rebuild_and_verify_caches/0 after writing all
    nodes and arcs.  rebuild_and_verify_caches/0 invokes
    graphdb_mgr:rebuild_caches/0 then graphdb_mgr:verify_caches/0;
    a verify mismatch throws {bootstrap_cache_invariant_failed,
    Mismatches} as a fatal startup error.

graphdb_bootstrap_tests:
  - All 6-tuple node terms in classify_terms/sort/validate/
    term_to_node tests rewritten to 5-tuples.
  - List-comprehension matchers updated from {node,_,K,_,_,_} to
    {node,_,K,_,_}.
  - term_to_node tests now expect parents=[] (the rebuild step
    happens at load/0 level, outside term_to_node's contract).

graphdb_bootstrap_SUITE:
  - Two error-path tests (load_missing_nref_start,
    load_nref_above_floor) updated to write the new tuple shape.

Tests: 122 CT + 64 EUnit = 186 all green.  graphdb_bootstrap_SUITE
load tests still see correct parents/classes caches because the
new rebuild step populates them; the existing
load_root_node_correct, load_attribute_node_correct,
load_template_avp_node_correct, and load_category_children
testcases all continue to pass with the same expected values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the H0 series.  The "arcs authoritative; hierarchy lists
cached" invariant is now a first-class architectural property of
the kernel, summarised in ARCHITECTURE.md and enforced by
graphdb_mgr:verify_caches/0 at every CT testcase boundary and the
bootstrap loader's tail.

ARCHITECTURE.md updates:
  - Status table: 186 tests (was 156); graphdb_mgr line names cache
    audit/repair as a feature.
  - §2 Storage indexes: drop the retired nodes parent index; document
    the arc-based downward-query path.
  - §3 Node Record: new record schema (parents/classes lists);
    new "Cache invariant" subsection covering the three rules,
    cache-field source table, single-writer ownership, and the
    verify_caches/0 / rebuild_caches/0 audit APIs.  Adds template
    to the node-kinds table.
  - §8 Bootstrap: 5-tuple node format documented; loader description
    notes the rebuild + verify pass and the
    bootstrap_cache_invariant_failed startup error.
  - §9 Inheritance: Priority 3 walks node.parents (single-chain today,
    DAG via H3); H1+H2 dropped from "known correctness gaps" since
    they landed in a078373.
  - §10 Open Questions: "Composition: dual storage" removed (resolved
    by H0); multi-inheritance entry rephrased to reflect that the
    cache-list infrastructure already landed.

arcs-authoritative.md: status flipped to Accepted with the H0a-H0e
commit chain; "Future work" notes the doc fold.

TASKS-HIGH.md: H0 marked RESOLVED with a commit-by-commit summary.
The header preamble now reflects that H0 has landed (was: "in
flight"); H3-H5 framed as additive work atop the cache
infrastructure rather than tangled with schema migration.

TASKS-MEDIUM.md: M1 marked RESOLVED, pointing at H0 + the
arcs-authoritative.md / ARCHITECTURE.md §3 references.

CLAUDE.md (root + apps/graphdb): node record schema updated;
apps/graphdb/CLAUDE.md describes the cache-invariant + audit APIs in
place of the retired parent index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@david-w-t david-w-t merged commit 4e56761 into davidwt-com:main May 8, 2026
1 check passed
david-w-t added a commit to david-w-t/SeerStoneGraphDb that referenced this pull request May 9, 2026
All H-tasks (H0a-H0e, H1+H2, H3, H4+H5) landed in PRs davidwt-com#10 and davidwt-com#12.
Per the task-file retirement workflow (close-out PR keeps the file
with finish markers; the next PR opens with a removal commit), drop
the file and prune cross-references in CLAUDE.md, README.md,
ARCHITECTURE.md, arcs-authoritative.md, TASKS-MEDIUM.md, and the
two app-level CLAUDE.md files.
david-w-t added a commit that referenced this pull request May 9, 2026
* M4: atomic reciprocal attribute pair

graphdb_attr:create_relationship_attribute/3 now writes both attribute
nodes and all four compositional arc rows inside a single Mnesia
transaction.  Previously the forward and reciprocal nodes were created
in separate transactions; if the second aborted, the database was left
with an orphan forward attribute and no usable reciprocal.

New private helper do_create_relationship_attribute_pair/3 allocates
the 2 node nrefs and 4 arc-id nrefs outside the transaction (avoiding
side-effects on retry) and writes 6 rows in one txn.

Test: create_relationship_attribute_pair_atomic asserts the row deltas
on `nodes` and `relationships` after a successful pair creation are
exactly +2 and +4 respectively, and that both nodes have exactly one
parent->child arc into them under nref 8.

Tests green: 146 CT + 64 EUnit = 210.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* M3: validate add_relationship endpoints and target_kind

graphdb_instance:add_relationship now runs an explicit validation pass
before resolving classes/templates and writing arcs.  Five new failure
modes, all returning structured {error, _} tuples:

  - {source_not_found, Nref}
  - {target_not_found, Nref}
  - {characterization_not_found, Nref}
  - {reciprocal_not_found, Nref}
  - {characterization_not_an_attribute, Nref, ActualKind}
  - {reciprocal_not_an_attribute, Nref, ActualKind}
  - {target_kind_mismatch, ExpectedKind, ActualKind}

The four endpoint reads run in one mnesia:transaction.  target_kind is
sourced from the seeded `target_kind` literal-attribute (graphdb_attr);
the nref is fetched once at graphdb_instance init via
graphdb_attr:seeded_nrefs() and cached in a new gen_server state
record.  Arc-label nodes that lack a target_kind AVP (relationship-
type bucket nodes, legacy data) skip the kind check.

The state record is a clean break from the previous `[]` carrier; only
the new validation needs it, but threading it through add_relationship
keeps the gen_server signature consistent.

Tests: +5 CT cases under the `relationships` group covering missing
source/target, non-attribute characterization, non-attribute
reciprocal, and target_kind mismatch (target_kind=class vs. instance
target).  151 CT + 64 EUnit = 215 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* M5: per-arc AVPs at connection creation

graphdb_instance:add_relationship/6 (S, C, T, R, TemplateNref,
{FwdAVPs, RevAVPs}) -> ok | {error, _} accepts per-direction user AVPs
and stamps them on the connection rows alongside the auto-applied
Template AVP.  Per-direction (asymmetric) is required by §5: connection
metadata such as provenance, confidence, weights, and validity windows
is direction-specific.

The Template AVP stays at index 0 of each row's avps list; user AVPs
follow.  /4 and /5 stay non-breaking and pass {[],[]} to /6 internally.
write_connection_arcs is updated to take {FwdAVPs, RevAVPs} and the
gen_server message form gains AVPSpec as a 7th tuple element.

Tests: +3 CT cases under the `relationships` group:
  - add_relationship_stamps_user_avps -- user AVP is present alongside
    Template AVP on the forward row
  - add_relationship_avps_are_per_direction -- a forward-only AVP must
    not leak into the reverse row, and vice versa
  - add_relationship_default_avps_empty -- /4 still produces a row
    with exactly the Template AVP

154 CT + 64 EUnit = 218 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Docs: M3 + M4 + M5 closeout in TASKS-MEDIUM, refresh test counts

Mark M3, M4, M5 RESOLVED in TASKS-MEDIUM.md with status blocks
describing the API and test additions for each.  Bump README and
ARCHITECTURE test counts (209 -> 218; 145 CT -> 154 CT).  Update
graphdb_attr_SUITE row to mention atomic reciprocal pair, and
graphdb_instance_SUITE row to mention M3 validation and M5 per-arc
AVPs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Retire TASKS-HIGH.md

All H-tasks (H0a-H0e, H1+H2, H3, H4+H5) landed in PRs #10 and #12.
Per the task-file retirement workflow (close-out PR keeps the file
with finish markers; the next PR opens with a removal commit), drop
the file and prune cross-references in CLAUDE.md, README.md,
ARCHITECTURE.md, arcs-authoritative.md, TASKS-MEDIUM.md, and the
two app-level CLAUDE.md files.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant