From f9ba8162eca020a6942191829caa217b2264381b Mon Sep 17 00:00:00 2001 From: "David W. Thomas" Date: Thu, 11 Jun 2026 20:51:58 -0400 Subject: [PATCH 1/2] docs: reorganize tasks/architecture docs; drop task-label shorthand Move the two big reference docs and the resolved-task archive under docs/: ARCHITECTURE.md -> docs/Architecture.md the-knowledge-network.md -> docs/TheKnowledgeNetwork.md TASKS-DONE.md -> docs/archive/TASKS-DONE.md Rewrite TASKS.md to hold only remaining/deferred work, organized by area (rule engine, write-path, multi-project, operational) rather than the old F/L/M/E phase labels. Move every resolved item into the archive with its original phase labels and decision logs preserved. Strip task-label shorthand (F1, L3, M6, B2, H3, ...) from the root markdown, the docs top-level, and all code comments, keeping a brief descriptive phrase where the label carried meaning. Update every reference to the moved/renamed files across living docs, design/plan docs, the archive, and tooling indexes. Correct a few stale claims the labels were attached to (dictionary servers wired, mgr write-side delegated, rules engine past Phase A). Comment-only and doc-only changes. Compiles clean; 509 tests pass (404 CT + 105 EUnit). Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 46 +- README.md | 96 +- TASKS-DONE.md | 145 -- TASKS.md | 1212 +++-------------- apps/graphdb/CLAUDE.md | 6 +- apps/graphdb/src/graphdb_attr.erl | 4 +- apps/graphdb/src/graphdb_class.erl | 6 +- apps/graphdb/src/graphdb_instance.erl | 74 +- apps/graphdb/src/graphdb_language.erl | 2 +- apps/graphdb/src/graphdb_mgr.erl | 2 +- apps/graphdb/src/graphdb_query.erl | 37 +- apps/graphdb/src/graphdb_rules.erl | 57 +- apps/graphdb/test/graphdb_attr_SUITE.erl | 6 +- apps/graphdb/test/graphdb_class_SUITE.erl | 4 +- apps/graphdb/test/graphdb_instance_SUITE.erl | 92 +- apps/graphdb/test/graphdb_instance_tests.erl | 2 +- apps/graphdb/test/graphdb_mgr_SUITE.erl | 4 +- apps/graphdb/test/graphdb_query_SUITE.erl | 30 +- apps/graphdb/test/graphdb_rules_SUITE.erl | 56 +- arcs-authoritative.md | 34 +- ARCHITECTURE.md => docs/Architecture.md | 102 +- docs/CLAUDE.md | 16 + .../TheKnowledgeNetwork.md | 0 docs/archive/TASKS-DONE.md | 1091 +++++++++++++++ docs/resiliency-notes.md | 36 +- memory/project_m6_language_class_gap.md | 6 +- 26 files changed, 1631 insertions(+), 1535 deletions(-) delete mode 100644 TASKS-DONE.md rename ARCHITECTURE.md => docs/Architecture.md (88%) create mode 100644 docs/CLAUDE.md rename the-knowledge-network.md => docs/TheKnowledgeNetwork.md (100%) create mode 100644 docs/archive/TASKS-DONE.md diff --git a/CLAUDE.md b/CLAUDE.md index 152cb19..b3f4a7a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ SeerStoneGraphDb/ ├── rebar.config # rebar3 umbrella build configuration ├── rebar.lock # Locked dependency versions ├── Makefile # Convenience targets (compile, shell, release, clean, rebar3) -├── ARCHITECTURE.md # High-level architecture; kept current with the code +├── docs/ # Architecture.md, TheKnowledgeNetwork.md, designs, archive └── CLAUDE.md # This file ``` @@ -63,14 +63,14 @@ graphdb (application — started after mnesia + nref) ├── graphdb_attr (gen_server — implemented: seeds + create/lookup API) ├── graphdb_class (gen_server — implemented: taxonomic hierarchy, QC inheritance) ├── graphdb_instance (gen_server — implemented: compositional hierarchy, inheritance) - ├── graphdb_language (gen_server — implemented: M6 multilingual overlay) - ├── graphdb_query (gen_server — implemented: F3 query language) - └── graphdb_rules (gen_server — implemented: F4 Phase A rule meta-ontology + create/retrieve) + ├── graphdb_language (gen_server — implemented: multilingual overlay) + ├── graphdb_query (gen_server — implemented: query language) + └── graphdb_rules (gen_server — implemented: rule meta-ontology, create/retrieve, composition + connection firing) dictionary (application — started alongside graphdb) └── dictionary_sup (supervisor) - ├── dictionary_server (gen_server — stub, not yet wired to dictionary_imp) - └── term_server (gen_server — stub, not yet wired to dictionary_imp) + ├── dictionary_server (gen_server — implemented: delegates to dictionary_imp) + └── term_server (gen_server — implemented: delegates to dictionary_imp) database (application — started after graphdb + dictionary) └── database_sup (supervisor) — empty; attachment point for future database-level services @@ -130,7 +130,7 @@ Maintain this structure when adding new modules. ## Knowledge Model This database is an implementation of the knowledge graph model described in -`the-knowledge-network.md` (sourced from US patents 5,379,366; +`docs/TheKnowledgeNetwork.md` (sourced from US patents 5,379,366; 5,594,837; 5,878,406 — Noyes; and Cogito knowledge center documentation). ### Core Concepts @@ -267,26 +267,24 @@ A logical bidirectional edge is two `relationship` rows written atomically (one | `graphdb_class` | Manages the taxonomic hierarchy: class nodes, qualifying characteristics, inheritance | | `graphdb_instance` | Creates and retrieves instance nodes; manages compositional hierarchy | | `graphdb_rules` | Stores and enforces graph rules (pattern recognition, relationship constraints) | -| `graphdb_language` | M6 multilingual overlay layer (label registration, dialect chains, per-language Mnesia overlay tables) | -| `graphdb_query` | F3 query language: parses and executes graph queries (Q1-Q6) against the node network | +| `graphdb_language` | Multilingual overlay layer (label registration, dialect chains, per-language Mnesia overlay tables) | +| `graphdb_query` | Query language: parses and executes graph queries against the node network | | `graphdb_mgr` | Primary coordinator: routes operations across the other six workers | ## Known Incomplete Areas (NYI) These are outstanding items — all previously known bugs have been fixed. -- **`graphdb_rules` rule-firing engine** — F4 Phases A, B1, B2, B3, and B4 are implemented (rule meta-ontology + create/retrieve; taxonomy walk; composition firing; propose mode; connection firing). Phase B5 (precedence) and Phases C–F remain outstanding (TASKS.md F4) -- **`graphdb_mgr` write operations** — `create_attribute/3`, `create_class/2`, `create_instance/3`, `add_relationship/4`, `delete_node/1`, `update_node_avps/2` return `{error, not_implemented}` pending L4 routing work -- **`dictionary_server` and `term_server`** — stubs not yet wired to `dictionary_imp` (TASKS.md Task 7) -- **`seerstone:start/2` and `nref:start/2`**, **`code_change/3`** — deferred (TASKS.md E2, E3) +- **`graphdb_rules` rule-firing engine** — the rule meta-ontology, create/retrieve, taxonomy walk, composition firing, propose mode, and connection firing are implemented. Conflict precedence and the later firing-engine phases (instantiation engine, reactive learning) remain outstanding (see `TASKS.md`) +- **`graphdb_mgr` write operations** — `create_attribute/3`, `create_class/2`, `create_instance/3`, `add_relationship/4` delegate to the workers; `delete_node/1` and `update_node_avps/2` still return `{error, not_implemented}` pending a worker that implements them +- **`code_change/3`** — deferred in every gen_server until the first hot-upgrade deployment (see `TASKS.md`) - **App lifecycle callbacks** — `start_phase/3`, `prep_stop/1`, `stop/1`, `config_change/3` return `ok` (no-op) across all five app modules; correct for current deployment model ## Remaining Work -Remaining tasks are in `TASKS.md` (feature phases F1–F4 and -Engineering Hygiene). Critical schema-level work is complete (PR #9); -high-severity inheritance/membership correctness work landed in PR -#12. +Remaining tasks are in `TASKS.md`. Critical schema-level work is +complete (PR #9); high-severity inheritance/membership correctness +work landed in PR #12. ## Configuration @@ -321,11 +319,11 @@ GitHub Actions workflow at `.github/workflows/ci.yml`: ## Documentation -`ARCHITECTURE.md` must reflect the current high-level shape of the code. -Keep it current — but at architectural altitude, not implementation -detail. +`docs/Architecture.md` must reflect the current high-level shape of the +code. Keep it current — but at architectural altitude, not +implementation detail. -**Update `ARCHITECTURE.md` when:** +**Update `docs/Architecture.md` when:** - The Mnesia schema changes (record fields added/removed/renamed). - The OTP supervision tree changes (new/removed workers, supervisor reorganisation). @@ -335,7 +333,7 @@ detail. - An architectural decision is made or revised (storage technology, cross-module routing, identity/allocation strategy). -**Don't update `ARCHITECTURE.md` for:** +**Don't update `docs/Architecture.md` for:** - Internal refactors that don't change the contract. - Bug fixes, style changes, comment edits, test additions. - Implementation progress within an already-described component. @@ -356,8 +354,8 @@ that file must reflect the current shape of the tree. environment-only). - Internal refactors that leave the seed shape unchanged. -The canonical spec is `the-knowledge-network.md` — it does **not** track -the code. Outstanding work lives in `TASKS.md`. +The canonical spec is `docs/TheKnowledgeNetwork.md` — it does **not** +track the code. Outstanding work lives in `TASKS.md`. ## Storage Technologies Used diff --git a/README.md b/README.md index a8c36b4..1712684 100644 --- a/README.md +++ b/README.md @@ -13,24 +13,24 @@ and extend his work. PRs are welcome. ### Current Status The project compiles clean with zero warnings (OTP 27 / rebar3 3.24). The -architecture is fully designed (see `ARCHITECTURE.md`). Implementation is -underway: - -| Component | Status | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `nref` subsystem | Fully implemented (DETS — Disk-based Erlang Term Storage — backed ID allocator with `set_floor/1`) | -| `dictionary` subsystem | `dictionary_imp` implemented; server stubs not yet wired (Task 7) | -| `graphdb_bootstrap` | Fully implemented — Mnesia (Erlang's built-in distributed database) schema/table creation, bootstrap scaffold loader (38 nodes, 38 relationship pairs) | -| `graphdb_mgr` | Implemented — bootstrap init, public read API (`get_node`, `get_relationships`), category immutability guard, cache audit/repair (`verify_caches/0`, `rebuild_caches/0`); write operations delegate to workers | -| `graphdb_attr` | Fully implemented — attribute library (name, literal, relationship attributes, relationship types) | -| `graphdb_class` | Fully implemented — taxonomic hierarchy with multi-parent inheritance (BFS — breadth-first search — over a DAG, a directed acyclic graph), qualifying characteristics, class-level inheritance | -| `graphdb_instance` | Fully implemented — compositional hierarchy, multi-class membership, four-level inheritance with class-resolver ambiguity detection; fires composition rules on `create_instance/3` (F4 B2), surfaces `proposed` outcomes for propose-mode rules (F4 B3), and fires connection rules via a caller-supplied resolver on `create_instance/4` (F4 B4) | -| `graphdb_rules` | Implemented — F4 Phases A+B1+B2+B3+B4: rule meta-ontology + create/retrieve; `effective_rules_for_class/2` + `effective_connection_rules/2` (taxonomy walk); composition firing engine; propose mode; connection firing; firing engine (Phase B5 precedence + C–F) outstanding (TASKS.md F4) | -| `graphdb_language` | Fully implemented — M6 multilingual overlay (language registration, dialect chains, per-language overlay tables, label resolution, translation hooks) | -| `graphdb_query` | Implemented — F3 query language (parse/execute, snapshot-semantics sessions, path finding) | +architecture is fully designed (see [`docs/Architecture.md`](docs/Architecture.md)). +Implementation is underway: + +| Component | Status | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `nref` subsystem | Fully implemented (DETS — Disk-based Erlang Term Storage — backed ID allocator with `set_floor/1`) | +| `dictionary` subsystem | `dictionary_imp` implemented; `dictionary_server` / `term_server` wired to it | +| `graphdb_bootstrap` | Fully implemented — Mnesia (Erlang's built-in distributed database) schema/table creation, bootstrap scaffold loader (38 nodes, 38 relationship pairs) | +| `graphdb_mgr` | Implemented — bootstrap init, public read API (`get_node`, `get_relationships`), category immutability guard, cache audit/repair (`verify_caches/0`, `rebuild_caches/0`); write operations delegate to workers | +| `graphdb_attr` | Fully implemented — attribute library (name, literal, relationship attributes, relationship types) | +| `graphdb_class` | Fully implemented — taxonomic hierarchy with multi-parent inheritance (BFS — breadth-first search — over a DAG, a directed acyclic graph), qualifying characteristics, class-level inheritance | +| `graphdb_instance` | Fully implemented — compositional hierarchy, multi-class membership, four-level inheritance with class-resolver ambiguity detection; fires composition rules on `create_instance/3`, surfaces `proposed` outcomes for propose-mode rules, and fires connection rules via a caller-supplied resolver on `create_instance/4` | +| `graphdb_rules` | Implemented — rule meta-ontology + create/retrieve; `effective_rules_for_class/2` + `effective_connection_rules/2` (taxonomy walk); composition firing engine; propose mode; connection firing; conflict precedence and the later firing-engine phases outstanding (see `TASKS.md`) | +| `graphdb_language` | Fully implemented — multilingual overlay (language registration, dialect chains, per-language overlay tables, label resolution, translation hooks) | +| `graphdb_query` | Implemented — query language (parse/execute, snapshot-semantics sessions, path finding) | **509 tests** (105 EUnit + 404 Common Test) — all passing. See -`TASKS.md` for the prioritised task list. +`TASKS.md` for remaining work. --- @@ -128,7 +128,7 @@ nref (application — started independently) ## Knowledge Model -The architecture is described in [`the-knowledge-network.md`](the-knowledge-network.md), +The architecture is described in [`docs/TheKnowledgeNetwork.md`](docs/TheKnowledgeNetwork.md), derived from US patents 5,379,366; 5,594,837; 5,878,406 (Noyes) and Cogito knowledge center documentation. @@ -192,15 +192,15 @@ Priority order — each step applies only to attributes not yet resolved by a hi ### graphdb Workers -| Module | Role | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `graphdb_attr` | Attribute library — name attributes, literal attributes, relationship attributes, relationship types | -| `graphdb_class` | Taxonomic hierarchy — class nodes, qualifying characteristics, class inheritance | -| `graphdb_instance` | Instance nodes — creation, retrieval, compositional hierarchy | -| `graphdb_rules` | Graph rules — F4 rule meta-ontology + create/retrieve; `effective_rules_for_class/2` (B1); composition firing engine (B2); propose mode (B3); Phases B4–F outstanding | -| `graphdb_language` | Multilingual overlay (M6) — language registration, dialect chains, per-language overlay tables, label resolution | -| `graphdb_query` | Query language (F3) — parses and executes graph queries; snapshot-semantics sessions | -| `graphdb_mgr` | Primary coordinator — routes operations across the other specialized workers | +| Module | Role | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `graphdb_attr` | Attribute library — name attributes, literal attributes, relationship attributes, relationship types | +| `graphdb_class` | Taxonomic hierarchy — class nodes, qualifying characteristics, class inheritance | +| `graphdb_instance` | Instance nodes — creation, retrieval, compositional hierarchy | +| `graphdb_rules` | Graph rules — rule meta-ontology + create/retrieve; taxonomy-walk effective-rules reads; composition firing engine; propose mode; connection firing; conflict precedence and later phases outstanding | +| `graphdb_language` | Multilingual overlay — language registration, dialect chains, per-language overlay tables, label resolution | +| `graphdb_query` | Query language — parses and executes graph queries; snapshot-semantics sessions | +| `graphdb_mgr` | Primary coordinator — routes operations across the other specialized workers | --- @@ -232,26 +232,26 @@ Priority order — each step applies only to attributes not yet resolved by a hi ./rebar3 eunit --app=graphdb && ./rebar3 ct ``` -| Suite | Type | Tests | Coverage | -| ------------------------- | ----- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `graphdb_bootstrap_tests` | EUnit | 61 | Term parsing, validation, record conversion, nref macro consistency | -| `graphdb_class_tests` | EUnit | 13 | `is_valid_parent_kind/1`, `collect_qc_nrefs/2` | -| `graphdb_instance_tests` | EUnit | 13 | `find_avp_value/2`, composition-firing helpers (`summarize/1` etc.) | -| `graphdb_language_tests` | EUnit | 9 | Dialect-chain building, label-resolution helpers (M6) | -| `graphdb_mgr_tests` | EUnit | 9 | Direction validation, client-side arg checks | -| `graphdb_bootstrap_SUITE` | CT | 19 | Full bootstrap load, Mnesia tables, idempotency, error handling, Language subcategory nodes | -| `graphdb_mgr_SUITE` | CT | 28 | Bootstrap init, read ops, category guard, write stubs, cache audit/repair | -| `graphdb_attr_SUITE` | CT | 37 | Attribute create/lookup, seeding, relationship types, atomic reciprocal pair (M4), literal sub-groups, `attribute_type`/`instantiable` markers | -| `graphdb_class_SUITE` | CT | 49 | Class create, QC (qualifying characteristics), lookups, hierarchy, multi-inheritance (H3), inheritance, templates, abstract classes (L9) | -| `graphdb_instance_SUITE` | CT | 101 | Instance create (incl. F4 B2 composition rule firing, B3 propose-mode outcomes, B-prep `{Min,Max}` multiplicity, and B4 connection firing — resolver-driven mandatory/auto/propose, target validation), relationships (incl. M3 validation, M5 per-arc AVPs — attribute-value pairs), lookups, hierarchy, four-level inheritance, multi-class membership (H4 + H5) | -| `graphdb_language_SUITE` | CT | 27 | M6 multilingual overlay: language/dialect registration, per-language overlay tables, label resolution, translation hooks | -| `graphdb_query_SUITE` | CT | 43 | F3 query language: parse/execute, snapshot-semantics sessions, `#cont_path{}` resume, path finding | -| `graphdb_rules_SUITE` | CT | 71 | F4 rule meta-ontology seeding (incl. B4 `reciprocal_nref` literal), composition/connection rule create/retrieve (B4 reciprocal param), validation catalog (incl. `{Min,Max}` multiplicity range — B-prep), `effective_rules_for_class/2` taxonomy walk (B1), `effective_connection_rules/2` (B4), composition firing engine (B2), propose mode (B3) | -| `graphdb_nref_SUITE` | CT | 6 | Switchable node-nref allocation facade; permanent/runtime phase flip | -| `graphdb_nrefs_SUITE` | CT | 2 | `graphdb_nrefs:verify/0` bootstrap nref-macro consistency check | -| `rel_id_server_SUITE` | CT | 7 | Relationship-row ID allocator (`get_id/0`, `get_id_pair/0`) | -| `dictionary_server_SUITE` | CT | 7 | `dictionary_server` gen_server behaviour | -| `term_server_SUITE` | CT | 7 | `term_server` gen_server behaviour | +| Suite | Type | Tests | Coverage | +| ------------------------- | ----- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `graphdb_bootstrap_tests` | EUnit | 61 | Term parsing, validation, record conversion, nref macro consistency | +| `graphdb_class_tests` | EUnit | 13 | `is_valid_parent_kind/1`, `collect_qc_nrefs/2` | +| `graphdb_instance_tests` | EUnit | 13 | `find_avp_value/2`, composition-firing helpers (`summarize/1` etc.) | +| `graphdb_language_tests` | EUnit | 9 | Dialect-chain building, label-resolution helpers | +| `graphdb_mgr_tests` | EUnit | 9 | Direction validation, client-side arg checks | +| `graphdb_bootstrap_SUITE` | CT | 19 | Full bootstrap load, Mnesia tables, idempotency, error handling, Language subcategory nodes | +| `graphdb_mgr_SUITE` | CT | 28 | Bootstrap init, read ops, category guard, write stubs, cache audit/repair | +| `graphdb_attr_SUITE` | CT | 37 | Attribute create/lookup, seeding, relationship types, atomic reciprocal pair, literal sub-groups, `attribute_type`/`instantiable` markers | +| `graphdb_class_SUITE` | CT | 49 | Class create, QC (qualifying characteristics), lookups, hierarchy, multi-inheritance, inheritance, templates, abstract classes | +| `graphdb_instance_SUITE` | CT | 101 | Instance create (incl. composition rule firing, propose-mode outcomes, `{Min,Max}` multiplicity, and connection firing — resolver-driven mandatory/auto/propose, target validation), relationships (incl. arc validation, per-arc AVPs — attribute-value pairs), lookups, hierarchy, four-level inheritance, multi-class membership | +| `graphdb_language_SUITE` | CT | 27 | Multilingual overlay: language/dialect registration, per-language overlay tables, label resolution, translation hooks | +| `graphdb_query_SUITE` | CT | 43 | Query language: parse/execute, snapshot-semantics sessions, `#cont_path{}` resume, path finding | +| `graphdb_rules_SUITE` | CT | 71 | Rule meta-ontology seeding (incl. `reciprocal_nref` literal), composition/connection rule create/retrieve (incl. reciprocal param), validation catalog (incl. `{Min,Max}` multiplicity range), `effective_rules_for_class/2` taxonomy walk, `effective_connection_rules/2`, composition firing engine, propose mode | +| `graphdb_nref_SUITE` | CT | 6 | Switchable node-nref allocation facade; permanent/runtime phase flip | +| `graphdb_nrefs_SUITE` | CT | 2 | `graphdb_nrefs:verify/0` bootstrap nref-macro consistency check | +| `rel_id_server_SUITE` | CT | 7 | Relationship-row ID allocator (`get_id/0`, `get_id_pair/0`) | +| `dictionary_server_SUITE` | CT | 7 | `dictionary_server` gen_server behaviour | +| `term_server_SUITE` | CT | 7 | `term_server` gen_server behaviour | Each CT test case runs in an isolated Mnesia database with a fresh nref allocator in a private temp directory. @@ -325,12 +325,12 @@ controlled by `logger_level` in `config/sys.config`. See `CLAUDE.md` for detailed coding conventions, the NYI/UEM macro pattern, module header format, naming conventions, and the git workflow. See -`TASKS.md` for the prioritised list of remaining implementation work. +`TASKS.md` for the list of remaining implementation work. Key conventions at a glance: - Every module uses `?NYI(X)` and `?UEM(F, X)` macros for unimplemented paths - Module names follow the pattern: `name.erl`, `name_sup.erl`, `name_server.erl`, `name_imp.erl` - Graph nodes are identified by **Nrefs** — plain positive integers allocated by `nref_server:get_nref/0` -- See `the-knowledge-network.md` for the knowledge model behind the graphdb workers +- See [`docs/TheKnowledgeNetwork.md`](docs/TheKnowledgeNetwork.md) for the knowledge model behind the graphdb workers - PRs target `main` diff --git a/TASKS-DONE.md b/TASKS-DONE.md deleted file mode 100644 index 70bcb0e..0000000 --- a/TASKS-DONE.md +++ /dev/null @@ -1,145 +0,0 @@ - - -# SeerStoneGraphDb — Resolved Tasks - -Archive of completed work. Entries are in the order they were resolved. -See `TASKS.md` for active remaining work. - ---- - -## M1. PART-OF stored in two places with no consistency invariant — RESOLVED - -**Status:** Closed by H0 (PR #10, commit `4e56761`). The decision: -arcs are authoritative, `node.parents`/`node.classes` are caches with -a hard invariant enforced by `graphdb_mgr:verify_caches/0` (run in -every CT `end_per_testcase` and at bootstrap load completion). -Single-writer ownership rule documented in `arcs-authoritative.md` -and `ARCHITECTURE.md` §3. - ---- - -## M2. `resolve_from_class` should consult `graphdb_class`, not Mnesia directly — RESOLVED - -**Status:** Closed by H1. `resolve_from_class` now drives the class -walk through `graphdb_class:get_class/1` and -`graphdb_class:ancestors/1` instead of reading the `nodes` table -directly; the membership arc lookup reuses `do_class_of/1` so -`?CLASS_MEMBERSHIP_ARC` is no longer hard-coded inside the resolver. - ---- - -## M3. `add_relationship/4` validates nothing — RESOLVED - -**Status:** Fixed. `graphdb_instance:add_relationship` now runs an -explicit `validate_arc_endpoints/5` pass before resolving classes, -templates, and writing arcs. All four endpoint reads happen in one -`mnesia:transaction/1`. Failure modes are returned as structured -errors: - - - `{error, {source_not_found, Nref}}` - - `{error, {target_not_found, Nref}}` - - `{error, {characterization_not_found, Nref}}` - - `{error, {reciprocal_not_found, Nref}}` - - `{error, {characterization_not_an_attribute, Nref, ActualKind}}` - - `{error, {reciprocal_not_an_attribute, Nref, ActualKind}}` - - `{error, {target_kind_mismatch, ExpectedKind, ActualKind}}` - -The seeded `target_kind` literal-attribute nref is fetched from -`graphdb_attr:seeded_nrefs()` once at `graphdb_instance:init/1` and -cached in a new gen_server state record. Arc-label nodes that don't -carry a `target_kind` AVP skip the kind check. - -Tests: 5 CT cases under the `relationships` group covering the new -reject paths. - ---- - -## M4. Reciprocal attribute pair must be created in one transaction — RESOLVED - -**Status:** Fixed. `graphdb_attr:create_relationship_attribute/3` now -delegates to a private `do_create_relationship_attribute_pair/3` helper -that allocates the 2 node nrefs and 4 compositional arc-id nrefs -outside the transaction (avoiding side-effects on retry) and writes -all 6 rows in a single `mnesia:transaction/1`. Mid-pair aborts can no -longer leave the database with an orphan half-pair. - -Tests: CT case `create_relationship_attribute_pair_atomic` asserts -the row deltas are exactly +2 nodes and +4 relationships after a -successful call, and that both new nodes have exactly one parent→child -arc into them under the Relationships subtree (nref 8). - ---- - -## M5. `add_relationship` cannot accept per-arc AVPs at creation — RESOLVED - -**Status:** Fixed. New API -`graphdb_instance:add_relationship/6 :: (Source, Char, Target, Reciprocal, -TemplateNref, {FwdAVPs, RevAVPs}) -> ok | {error, _}` accepts -per-direction user AVPs and stamps them on the two connection rows -alongside the auto-applied Template AVP. Per-direction 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. - -Tests: 3 CT cases under the `relationships` group: -- `add_relationship_stamps_user_avps` -- `add_relationship_avps_are_per_direction` -- `add_relationship_default_avps_empty` - ---- - -## M7. Template support — RESOLVED - -**Spec:** §7 — *"A **template** is a named semantic context defined on -a class in the ontology. ... Not a blank form waiting to be filled — -it is an active node in the ontology."* - -**Status:** Substantively landed during the H-task series alongside -the Connection-arc and Template-AVP work. What landed: - - - 5th node kind: `kind = template`. Validated by - `graphdb_bootstrap:kind_order/1` (template = 5). - - Bootstrap node 31 — `Template` AVP-marker attribute, parented to - nref 16 (Instance Relationships). Stamped with - `relationship_avp => true` post-bootstrap. - - **Per-class templates**: templates are written as compositional - children of their owning class. Each `create_class/2` automatically - attaches a `"default"` template; class authors may `add_template/2` - more, or delete the default to force explicit template specification - on every connection arc. - - Public API on `graphdb_class`: `add_template/2`, `get_template/1`, - `templates_for_class/1`, `default_template/1`, - `class_in_ancestry/2`. - - Template-scoped `add_relationship` on `graphdb_instance`: `/4` - resolves the source class's default template; `/5` accepts an - explicit `TemplateNref`; `/6` adds per-direction user AVPs (M5). - Template AVP `#{attribute => 31, value => TemplateNref}` is stamped - at index 0 of each connection row's AVP list. Out-of-scope templates - produce `{error, {template_class_not_in_ancestry, ...}}`. - -Tests: `graphdb_class_SUITE` templates group (7 cases), -`graphdb_instance_SUITE` connection-arc cases (4 cases), -`graphdb_attr_SUITE` seeding group (2 cases), -`graphdb_bootstrap_tests.erl` kind_order cases. - ---- - -## M8. Attribute "type" implied by parent subtree — RESOLVED - -**Status:** Fixed via AVP-based marker. `graphdb_attr` seeds a fourth -runtime literal attribute, `attribute_type`, alongside `literal_type`, -`target_kind`, and `relationship_avp`. All `create_*` paths stamp an -`#{attribute => attribute_type_nref, value => name|literal|relationship}` -AVP on the new node. Bootstrap attribute nodes (nrefs 6–31) are -retro-stamped at `graphdb_attr:init/1` by walking the `parents` cache. - -New public API: `graphdb_attr:attribute_type_of/1` returns -`{ok, name | literal | relationship}` directly from the AVP. - -Tests: 10 CT cases under the `attribute_type` group. diff --git a/TASKS.md b/TASKS.md index dabbdde..49815b2 100644 --- a/TASKS.md +++ b/TASKS.md @@ -5,1070 +5,198 @@ SPDX-License-Identifier: GPL-2.0-or-later # SeerStoneGraphDb — Remaining Tasks -Organized by execution sequence. Feature phases must land in the order -shown — each gates the next. Engineering Hygiene tasks have no blocking -dependencies and can be interleaved at any point. +What is left to build, grouped by area. None of these blocks the others +in a strict gate the way earlier feature phases did; the rule-engine +items build on one another, but the write-path, multi-project, and +operational tracks are independent and can be picked up in any order. -Resolved tasks are archived in `TASKS-DONE.md`. +The kernel is functional: bootstrap, the attribute library, the +taxonomic and compositional hierarchies with four-level inheritance, +templates, the multilingual overlay, the query language, and the rules +data model with its composition and connection firing engines have all +landed. Completed work — with its original phase labels and decision +logs — is archived in [`docs/archive/TASKS-DONE.md`](docs/archive/TASKS-DONE.md). ---- - -## Feature Track - ---- - -## F1. Language Ontology Bootstrap — RESOLVED - -Gate: must land before F2. `the-knowledge-network.md` §15 now documents -Languages as any communication form with grammar, syntax, and tokens or -icons — significantly broader than human natural languages alone. Four -top-level categories belong under the Languages node (nref 4): - -**Status:** Complete. Nrefs 32–35 seeded in `bootstrap.terms`. CT -coverage in `graphdb_bootstrap_SUITE` (`load_language_subcategories`). - -- Human Languages — written and verbal natural languages -- Formal Languages — programming languages, query languages, - mathematical notation -- Diagram Languages — UML, engineering schematics, tabular forms, - hierarchical diagrams -- Renderers — shared rendering engines (also: views) - -The current `bootstrap.terms` has no named subcategories under nref 4. -This task adds them, updates all dependents, and resolves any code -implications before F2 coding begins. - -### Planning step - -Output: nref assignments + any new sub-tasks appended to Engineering -Hygiene. Audit before writing code: - -1. All code that references nref 4 or the Languages subtree by nref - constant — note what each piece needs. -2. All CT assertions on exact bootstrap node/arc counts — these will - need updating (+4 nodes, +8 arcs minimum). -3. Final nref assignments for the four new category nodes (candidates: - 32–35; confirm no conflicts with existing bootstrap or seeded nrefs). -4. Any deferred follow-up tasks surfaced — append to Engineering - Hygiene below. - -Record nref assignments in the CLAUDE.md Bootstrap Nref -Quick-Reference table and cerebrum.md before writing any code. - -### Execution - -1. `apps/graphdb/priv/bootstrap.terms` — add four category nodes and - eight compositional arc rows (two per parent/child pair, connecting - each new node to Languages nref 4). Arc labels: ChildArc=22, - ParentArc=21; `kind=composition` — same pattern as all other - category arcs. - -2. `CLAUDE.md` — update Bootstrap Nref Quick-Reference table with the - four new entries. - -3. CT suites asserting exact node/arc counts — update expectations. - -4. This file (F2, M6-B and M6-D) — update English concept node seeding - target to Human Languages (assigned nref), not nref 4 directly. - -**Dependencies:** none upstream. Gates F2. - ---- - -## F2. M6 — Multilingual Layer — RESOLVED - -**Status:** Complete. `graphdb_language` is a fully implemented gen_server. -24/24 CT tests pass (`graphdb_language_SUITE`). 192/192 CT total, 99 EUnit, -zero warnings. M6-I (write-path integration) is explicitly deferred — it -depends on L4 (wire `graphdb_mgr` write-side). All Architecture Review issues -R1–R10 are resolved or closed. See Decision Log below. - -**Depends on F1.** - -**Spec:** §15 — *"Concepts are stored language-neutrally in the -ontology. Labels, prompts, and vocabulary entries are stored per -language and swapped at rendering time without modifying the -knowledge."* (§15 > Human Languages) - -**Design:** The environment node record (`#node{}`) is unchanged and -the environment database is the English default — the terminal fallback -for every language chain. English is the practical common language of -international communication; it is acknowledged as the environment's -base language without apology. Language-specific labels are stored in -per-language Mnesia tables within the same schema (overlay model). A -language chain is a runtime parameter scoped to the session, user, or -use case; resolution walks the chain left-to-right and falls through to -the environment node on miss. Per-AVP override semantics: a language -overlay record carries only the AVPs it overrides; all other AVPs -resolve from the environment node unchanged. - -Dialect distinctions are optionally supported. A dialect code such as -`en_gb` or `pt_br` (atom, underscore convention) identifies a -finer-grained overlay table. Dialect overlays carry only terms that -genuinely differ from the base language; most terms fall through to the -base language or the environment. Dialectal variants are an explicit -authoring decision — the system never infers which dialect a string -belongs to. - -Project databases mirror this model. The terminal fallback is the -project node record, authored in an implementer-chosen language. That -language is specified as an AVP on the project root node, referencing a -language concept node in the environment. - -**Completed state:** `graphdb_language.erl` is a full gen_server -implementation covering M6-A through M6-H and M6-J. `bootstrap.terms` -carries English strings as node AVPs — these are the English default and -require no migration. M6-I is deferred to L4. - ---- - -### Sub-tasks - -> **Pre-implementation gate:** Blockers R1–R4 in the Architecture -> Review section below must each have a recorded decision before any -> sub-task here is coded. R1 (project nref collision) and R2 (dialect -> algorithm) affect API signatures and test cases; resolving them first -> prevents cascading rework. - -**M6-A: Language overlay record and Mnesia schema** - -```erlang --record(language_node, { - nref, %% integer() — same keyspace as environment nodes table - avps %% [#{attribute => AttrNref, value => Value}] - %% — AVPs that shadow matching AVPs on the environment node -}). -``` - -One Mnesia `disc_copies` table per language or dialect -(`language_en`, `language_de`, `language_en_gb`, `language_pt_br`, …) -with `{record_name, language_node}`. Tables are created on demand when -a language or dialect is registered. `graphdb_bootstrap` creates -`language_en` at environment init; it will be mostly empty in practice -since the environment node record is itself the English default — the -table exists to make `en` a well-formed chain entry. - -> **R6 (should fix):** Specify runtime `mnesia:create_table/2` -> behaviour in `register_language/2` and `register_dialect/3` — -> synchronous vs. async, timeout, concurrent-registration safety across -> nodes. See Architecture Review. - -**M6-B: Language concept nodes** - -`graphdb_language:init/1` seeds a language concept node for English -under Human Languages (nref 32) using the standard -ensure-seed-by-name pattern. **Node kind: `instance`.** English is -already bootstrapped as `kind=instance` at nref 10000 (F2); all -language nodes (base languages, dialects) follow the same kind. -`kind=instance` eliminates the dual-mechanism risk: instances do not -participate in taxonomic IS-A arcs, so `base_language` AVP is the -sole authority for base/dialect relationships. The English nref is -cached in gen_server state and exposed via -`graphdb_language:seeded_nrefs/0`. - -Base languages and dialects are both language concept nodes, but -dialect nodes carry an AVP that references their base language concept -node: - -```erlang -#{attribute => base_language_nref, value => BaseLanguageConceptNref} -``` - -`base_language` is seeded as a literal attribute in -`graphdb_language:init/1` (same seeding pattern as `target_kind`). -Base language nodes carry no such AVP. This makes the base/dialect -relationship explicit, queryable, and independent of the atom naming -convention. - -> **R3: RESOLVED** — `kind=instance` for all language nodes. -> See Decision Log. -> -> **R4 (should fix):** Move `project_language` seeding from -> `graphdb_attr:init/1` to `graphdb_language:init/1` — owning worker -> pattern. See Architecture Review. - -**M6-C: Label resolver** - -```erlang -graphdb_language:resolve_label(Nref, AttrNref, Chain) -> Value | not_found -``` - -`Chain :: [atom()]` — language code atoms in priority order -(e.g., `[de, en_gb, en, fr]`). Walk: for each code in the chain: - - - If the code equals the environment's declared language (`en` by - default, readable from `graphdb_language:seeded_nrefs/0`): skip - the overlay table lookup and fall directly to the terminal node - read. This makes `en` a zero-cost sentinel — `language_en` is not - read, and the environment node record is used immediately. - - Otherwise: read `language_` table for Nref; if a record - exists and its `avps` contains AttrNref, return that value. - -If the chain is exhausted without a match, read from the terminal node -table (environment `nodes`, or project `nodes` for project nrefs). If -still absent, return `not_found`. - -For project nref resolution, the caller passes the project `nodes` -table name as an explicit terminal parameter — the resolver has no -global state about which database owns a given nref. - -> **R1 (blocker):** Signature is missing the terminal-table parameter, -> and project nrefs share the same integer keyspace as environment -> nrefs — a single `language_*` table keyed by nref alone cannot -> distinguish them. Resolve the project-side overlay story (shared vs. -> per-project tables, key scheme) and update the signature before -> coding. See Architecture Review. -> -> **R8 (should fix):** Specify where the environment's declared -> language code is stored (config, AVP, constant) — the sentinel -> optimisation depends on this lookup being authoritative and fast. -> See Architecture Review. - -**M6-D: Language registration** - -```erlang -%% Base language -graphdb_language:register_language(Code :: atom(), Name :: string()) - -> {ok, Nref} | {error, already_registered} | {error, _} - -%% Dialect — must name an already-registered base language -graphdb_language:register_dialect(Code :: atom(), Name :: string(), - BaseCode :: atom()) - -> {ok, Nref} | {error, base_not_found} - | {error, already_registered} - | {error, _} -``` - -Both create the concept node under Human Languages (nref 32) and its Mnesia overlay table. `register_dialect/3` additionally -stamps the `base_language` AVP on the dialect node, referencing the -base language concept nref. Calling `register_dialect/3` with an -unregistered `BaseCode` is an error. Both calls are idempotent on -restart (seed-by-name pattern). - -> **R6 (should fix):** Specify runtime `mnesia:create_table/2` -> behaviour — synchronous, timeout, concurrent-registration safety. -> See Architecture Review. - -**M6-E: Overlay write** - -```erlang -graphdb_language:set_labels(Nref, Code :: atom(), AVPs) -> ok | {error, _} -``` - -Writes or merges AVPs into the language overlay record for Nref in -`language_`. Merge semantics: existing AVPs for other attributes -on the same record are preserved; only the supplied AttrNrefs are -updated or added. - -**M6-F: Translation agent hook** - -```erlang -graphdb_language:register_translation_hook(Fun) -> ok -%% Fun :: fun((Nref :: integer(), DefaultAVPs :: [avp()]) -> ok) -``` - -Called after environment node creation with the new nref and its -English AVPs. Initially the hook list is empty (silent no-op). Multiple -hooks accumulate in registration order; all are called post-commit. -This is the designed insertion point for a future LLM-based translation -agent. As language overlays accumulate, translation patterns may emerge -and be encoded as rules — the hook is the path through which that -learning is initiated. - -> **R7 (should fix):** Hook must be invoked in a spawned process -> (`proc_lib:spawn/1`), never inline — inline blocks all callers and -> crashes the worker on exception. Add `unregister_translation_hook/1` -> for test cleanup. Clarify return-value contract (currently -> unspecified). See Architecture Review. - -**M6-G: Project default language** - -Seed `project_language` literal attribute in `graphdb_attr:init/1` -alongside `target_kind`, `relationship_avp`, `attribute_type`, and -`literal_type`. The project root node carries: - -```erlang -#{attribute => project_language_nref, value => LanguageConceptNref} -``` - -Public API: - -```erlang -graphdb_language:project_language(ProjectRootNref) - -> {ok, Code :: atom()} | not_found -``` - -Reads the `project_language` AVP from the project root node and -returns the language code atom for the referenced concept node. - -> **R1 (blocker):** Project-side overlay story unresolved — see M6-C -> callout and Architecture Review. This API cannot be fully specified -> until the project nref keyspace collision is resolved. - -**M6-H: Session chain helper** - -```erlang -graphdb_language:make_chain(Codes :: [atom()]) -> [atom()] -``` - -Validates each code against registered languages; drops unknown codes -with a log warning. Applies the dialect auto-insertion rule using the -following verified pseudocode: - -``` -make_chain(InputCodes): - ValidCodes = [C || C <- InputCodes, is_registered(C)] - Output = [] - Remaining = ValidCodes - while Remaining != []: - Code = head(Remaining) - Remaining = tail(Remaining) - Output = Output ++ [Code] % always emit - if is_dialect(Code): % concept node has base_language AVP - Base = base_language_of(Code) % AVP nref → concept node → lang_code atom - FullChain = Output ++ Remaining % current output (incl. Code) + remaining input - if Base not in FullChain: - Output = Output ++ [Base] % insert base immediately after dialect - return Output -``` - -The check is `Base not in (Output ++ Remaining)` — the full current -chain view, not just the output built so far. A base that still -appears later in the remaining input is not re-inserted. - -Verified derivations: - - - `[de, en_gb, fr]` → `[de, en_gb, en, fr]` (en∉[de,en_gb,fr] → insert) - - `[en_gb, en_us]` → `[en_gb, en, en_us]` (en∉[en_gb,en_us] → insert after en_gb; - en∈[en_gb,en,en_us] → skip after en_us) - - `[en_gb, en, fr]` → `[en_gb, en, fr]` (en∈[en_gb,en,fr] → skip) - - `[pt_br, de]` → `[pt_br, pt, de]` (pt∉[pt_br,de] → insert) - -Implementation notes: -- `base_language_of/1` does two Mnesia reads: concept-node-by-code → - `base_language` AVP nref → concept-node-by-nref → `lang_code` atom. - Cache results within a single `make_chain/1` call. -- `is_dialect/1` is a check for the presence of `base_language` AVP - on the concept node — no separate flag needed. - -Callers do not construct Mnesia table names directly. - -> **R2: RESOLVED** — pseudocode verified against all four examples. -> See Decision Log. - -**M6-I: Write-path integration** *(DEFERRED to L4)* - -Depends on L4 (wire `graphdb_mgr` write-side operations). When the -NYI write operations (`create_attribute`, `create_class`, -`create_instance`) are implemented, each must: - - 1. Create the environment node atomically in one Mnesia transaction. - 2. Call all registered translation hooks post-commit with the new - nref and its English AVPs. (Outside the transaction — best-effort.) - 3. If a session language list is provided with labels, call - `set_labels/3` for each language. (Also outside the transaction.) - -Steps 2–3 are not atomically coupled to step 1 by design. A failed -hook or missing language label does not roll back node creation. - -Dialect write discipline: do not auto-duplicate environment labels into -dialect overlay tables. A dialect overlay record is only written when -the label genuinely differs from the base language. The session -language list declares the context for new labels; deciding whether a -term warrants a dialect-specific override is an explicit authoring -decision, never inferred by the system. - -> **R1 (blocker):** Write-path integration for project instances cannot -> be specified until the project-side overlay story is resolved — -> including whether project-instance labels go into shared or -> per-project overlay tables. See Architecture Review R1 and R13. - -**M6-J: Tests** - -EUnit (`graphdb_language_tests.erl`) — pure function coverage: - - - `make_chain/1`: unknown codes silently dropped; known codes - preserved in order. - - `make_chain/1`: dialect auto-insertion — base inserted when absent - from chain as built so far (base determined from concept node AVP, - not atom parsing). - - `make_chain/1`: multiple dialects of same base — single insertion. - - `make_chain/1`: base already present in chain — no duplicate. - - `make_chain([])` → `[]`. - -CT (`graphdb_language_SUITE.erl`) — integration: - - - Register language → overlay table created; idempotent on - re-register. - - Register dialect → concept node carries `base_language` AVP - referencing base concept nref; `base_not_found` when base - unregistered. - - `set_labels/3` → AVP readable via `resolve_label/3`. - - `set_labels/3` with unregistered code → error, no write. - - Fallback: no overlay record → resolves from environment node. - - Chain priority: first-listed language wins over second. - - `en` sentinel: chain containing `en` reads environment node - directly; `language_en` table is not consulted. - - Dialect hit: `en_gb` overlay record returned when present; falls - through to environment when absent. - - Dialect fallback chain: `[en_gb, en, fr]` — `en_gb` miss → `en` - sentinel → environment node (skips `fr` because terminal matched). - - Project language AVP written and retrieved correctly. - - Translation hook: registered `Fun` called on node creation; empty - list is a silent no-op. - - Translation hook crash during node creation: creation must succeed. - - Re-register an already-registered language with a different name: - decide (error or overwrite) and test. - - Dialect node whose `base_language` AVP references a missing - concept: graceful resolution path. - - Mnesia transaction abort during `set_labels/3`: caller sees error, - no partial write. - -**Dependencies:** F1 must land first. Must land before F3 — query -render-time label resolution depends on the language overlay API. - ---- - -### Architecture Review — Open Issues - -Post-design audit conducted before implementation. Each blocker must be -resolved (with a decision recorded in the Decision Log) before any M6 -code is written. Should-fix items should be resolved during the -relevant sub-task. Notes are informational and do not block. - -#### Blockers - -**R1. RESOLVED** — `resolve_label/4` with `Scope :: environment | {project, AnchorNref}`. -Environment tables: `language_`. Project tables: `language__`. -`overlay_table_name/2` encodes both forms. M6-I (write-path integration) depends on -L4 (wire graphdb_mgr write-side) and is explicitly deferred. See Decision Log. - -**R2. RESOLVED** — Pseudocode verified in M6-H. The check is -`Base not in (Output ++ Remaining)` (full chain view). See Decision -Log. - -**R3. RESOLVED** — `kind=instance` for all language nodes. See -Decision Log. - -**R4. RESOLVED** — `project_language` seeded by -`graphdb_language:init/1`. Owning-worker pattern confirmed. See -Decision Log. - -#### Should Fix - -**R5. RESOLVED** — Environment stores English strings directly on -`#node{}` records (name AVPs on environment nodes). Documented -departure from the strict reading of §15. Rationale: English is the -environment's base language; reading it directly from the node record is -zero-overhead and the en sentinel in `do_resolve_chain/4` makes this -explicit by design, not accident. See Decision Log. - -**R6. RESOLVED** — `mnesia:create_table/2` called synchronously from -`ensure_overlay_table/1` during `register_language/2` and -`register_dialect/3`. The gen_server serialises all callers; no -concurrent registration races within a single node. Default Mnesia -timeout applies. Multi-node schema propagation is a known future -concern (R12 tracks table-count ceiling); acceptable for the -current single-node deployment model. See Decision Log. - -**R7. RESOLVED** — Hooks spawned via `proc_lib:spawn/1`; never inline. -Each hook body wrapped in try/catch; errors logged and discarded; -never propagated to the caller. `unregister_translation_hook/1` -added for test cleanup. See Decision Log. - -**R8. RESOLVED** — Environment language code stored as compile-time -macro `?ENV_LANGUAGE_CODE = en` in `graphdb_language.erl`. Exposed -via `seeded_nrefs/0` as `env_language_code => en` so callers and -tests can read it without a magic atom. See Decision Log. - -#### Notes - -**R9. Test coverage gaps resolved in M6-J above.** The cases missing -from the original spec have been folded into the M6-J test list. - -**R10. RESOLVED** — `en_gb` (underscore-separated lowercase atom) -chosen over IETF BCP 47 `en-GB`. Rationale: Erlang atoms cannot -contain unquoted hyphens; requiring quoted atoms (`'en-GB'`) would -make API usage awkward. Underscore-lowercase is idiomatic in Erlang. -The convention is documented; not a format bug. See Decision Log. - -**R11. No batch resolver API.** `resolve_label/3` is per-AVP. A -future `resolve_labels(Nref, [AttrNref], Chain) -> #{AttrNref => Value}` -will be wanted at F3 render time. Note for F3 planning; not blocking -M6. - -**R12. Mnesia table proliferation ceiling.** Default Mnesia schema -supports ~1024 tables. ISO 639 base codes (~200) plus dialects could -approach that in a fully-internationalised deployment. Most deployments -stay well under 20. Fallback design is a single `language_overlays` -table keyed by `{Code, Nref}`. Revisit if a deployment approaches the -ceiling. - -**R13. Project-side overlays absent from write-path plan.** *(M6-I)* - -The plan covers project *terminal fallback* but does not specify how -project-instance labels are written into overlay tables or retrieved. -Overlaps with blocker R1 — resolving R1 should also answer this. - -**R14. Snapshot consistency during render.** Multiple sequential -`resolve_label/3` calls while a concurrent `set_labels/3` is mid-flight -can return a mix of old and new values. Acceptable for labels; document -in the Decision Log so it is not later misconstrued as a correctness -bug. - -**R15. Translation hook return value contract undefined.** *(M6-F)* - -The signature `Fun :: fun((Nref, DefaultAVPs) -> ok)` implies the -return is always `ok`, but this is not enforced. Document explicitly -that the return value is discarded, or change the contract to -`-> ok | {error, Reason}` and specify what happens on error. - -#### Decision Log - -**R1 — Scope type: `environment | {project, AnchorNref}`** (2026-05-18) - -`resolve_label/4` takes a `Scope` argument distinguishing environment -reads (table `language_`) from project reads (table -`language__`). M6-I (project write-path) deferred -to L4; the scope type is already in the public API so the boundary is -clean when L4 lands. - -**R2 — Dialect auto-insertion uses full chain view** (2026-05-18) - -`do_make_chain/3` checks `Base not in (Output ++ Remaining)` — -the full chain view, not just the output so far. This ensures a base -already scheduled to appear later in the chain is not inserted early -and duplicated. Verified by hand-tracing `[en_gb, en_us]` with -`en_gb=>en, en_us=>en`. - -**R5 — English on env node records, not overlay table** (2026-05-18) - -English strings live on `#node{}` `attribute_value_pairs` fields, not -in a `language_en` Mnesia table. The `en` sentinel in -`do_resolve_chain/4` bypasses the overlay lookup and reads the node -directly. Rationale: zero-overhead for the most common case; avoids -duplicating every English label into a separate table at bootstrap. -This is a deliberate departure from the strict reading of §15 -("language-neutral storage") — English is the structural language of -the environment and is treated specially by design. - -**R6 — Synchronous overlay table creation, single-node only** (2026-05-18) - -`mnesia:create_table/2` is called synchronously inside the gen_server -handler for `register_language/2`. The gen_server serialises all -callers so there is no concurrent-registration race within a node. -Multi-node schema distribution is a known future concern deferred to -whenever multi-node support is added; for now the single-node model -is the only deployment target. - -**R7 — Hooks spawned, crash-safe, unregister for tests** (2026-05-18) - -Translation hooks are invoked via `proc_lib:spawn/1` so a slow or -crashing hook cannot block or kill the gen_server. Each hook body is -wrapped in try/catch; errors are logged and discarded. The return -value is always discarded. `unregister_translation_hook/1` exists -specifically so CT cases can clean up their hooks between test cases. - -**R8 — Environment language code as compile-time macro** (2026-05-18) - -`?ENV_LANGUAGE_CODE = en` is a module-level macro. Exposed through -`seeded_nrefs/0` as `env_language_code => en` so callers can read it -without an atom literal. Chosen over a config parameter because the -environment language is a structural invariant, not a deployment -setting — changing it would require re-bootstrapping the entire -environment. - -**R10 — Underscore-lowercase atom convention for locale codes** (2026-05-18) - -`en_gb` rather than `'en-GB'`. Erlang atoms containing hyphens must -be quoted; unquoted `en_gb` is idiomatic and avoids the quoting -requirement. All locale codes in the API follow this convention. -Applications bridging to BCP 47 external systems must translate at -the boundary. - ---- - -## F3. graphdb Query Language — RESOLVED - -Implemented as `graphdb_query` (the `graphdb_language` slot is occupied -by the M6 multilingual overlay layer). Design at -`docs/designs/f3-graphdb-query-design.md`; plan at -`docs/superpowers/plans/2026-05-23-f3-graphdb-query.md`. Seven query -primitives (Q1, Q1b, Q2-Q6), snapshot-semantics sessions, continuation -+ resume with `snapshot_expired` detection. Template-filtered -traversal lands in a future iteration alongside richer query criteria. - ---- - -## F4. E1 — `graphdb_rules` Rule Engine - -**Can start after F1. Parallel to F3 at discretion — E1 is large -scope; serial execution (F3 then F4) is a reasonable alternative.** - -**Spec:** §8 (rules as stored data), §9 (instantiation engine), §10 -(composition rules), §11 (reactive learning). - -The design splits E1 into six phases (A–F). See -`docs/designs/f4-graphdb-rules-design.md`. - -### F4 Phase A — Rule data model — **RESOLVED** (2026-06-02) - -`graphdb_rules` replaced its stub with the Phase A data model: a -runtime-seeded rule meta-ontology (`Rule` abstract class + -`CompositionRule` / `ConnectionRule` under nref 3; `Rule Literals` -sub-group + 6 literal attrs under nref 7; `applies_to` / `applied_by` -relationship-attribute pair under nref 16), and a scope-aware -create/retrieve API: `create_composition_rule/6,7`, -`create_connection_rule/7,8`, `get_rule/2`, `rules_for_class/2`, -`composition_rules_for_class/2`, `connection_rules_for_class/2`, -`list_rules/1`, `seeded_nrefs/0`. Content AVPs live on the rule -instance node; deployment AVPs (`mode`, `multiplicity`, `Template`) on -the `applies_to` connection arc. Retrieval is direct-attachment only. -`graphdb_rules` moved to the last child of `graphdb_sup`. 37 CT cases -added (`graphdb_rules_SUITE`). - -### F4 Phases B–F — Rule-firing engine — PARTIALLY DONE (B1+B2+B3 landed) - -The remaining phases build the engine that consumes the Phase A data -model: the instantiation engine, composition-rule firing at -`create_instance`, connection firing, conflict precedence, and reactive -learning. - -**Phase B is divided B1–B5** (each with its own design + plan): - -- **B1 — `effective_rules_for_class/2` (read-side taxonomy walk) — DONE.** - Nearest-first gather of every rule attached to a class and its taxonomy - ancestors, grouped by attaching class, each paired with its - `applies_to`-arc deployment (`mode`/`multiplicity`/`template`). Resolves - nothing — additive-vs-shadow is the firing engine's job. Design: - `docs/designs/f4-phase-b1-effective-rules-design.md`. -- **B2 — composition firing engine — DONE.** `create_instance/3` fires - `mandatory` rules inside the parent-creation transaction and `auto` rules - post-commit. Return shape is `{ok, Nref, Report}` / `{error, Reason, - Report}` (3-tuple on firing path). `plan_composition_firing/2` is a - pure-read helper reused by B3. Design: - `docs/designs/f4-phase-b2-composition-firing-design.md`. -- **B3 — `propose` mode — DONE.** `propose`-mode composition rules - materialise nothing; they surface as `proposed` outcomes in the - `create_instance/3` report (always-in-report — no session flag). A - caller accepts a proposal by issuing an ordinary `create_instance/3` - for the proposed class. Design: - `docs/designs/f4-phase-b3-propose-mode-design.md`. -- **B-prep — multiplicity-range refactor — DONE (PR #36).** Reshaped - `multiplicity` from `pos_integer() | unbounded` to a - `{Min, Max}` pair (`Min :: non_neg_integer()`, `Max :: pos_integer() | - unbounded`) across **both** composition and connection rules — uniform - rule shape; `unbounded` survives only as a value of `Max`, never - standalone, so deployment carries `{Min, Max}`. `mode` enforces the - floor (`mandatory` ⇒ ≥ `Min`); `Max` caps. Touches Phase A - `create_*_rule` signatures + validation, B1 `decode_deployment`, B2 - `plan_mandatory` / `expand_children`, and the CT suites (greenfield — - test churn only). Composition mint-from-range is **decided**: firing - mints `Min` (the floor drives the count); `Max` is the ceiling for a - future *interactive creation session* (human or autonomous agent tops up - optional children up to `Max`) — a separate later feature. Must land - before B4 (which consumes `{Min, Max}` deployment). See - `docs/designs/f4-phase-b4-connection-firing-design.md` §7 and B4-D5. -- **B4 — connection firing — DONE.** `create_instance/4` threads a - caller-supplied resolver; a RESOLVE step fires effective ConnectionRules - (`mandatory` in the root txn, `auto` post-commit, `defer`/`propose` - reported). ConnectionRule gains a `reciprocal_nref` content AVP; - `create_connection_rule/8,9` (reciprocal param) supersede `/7,8`; - `effective_connection_rules/2` is the read seam. Design - `docs/designs/f4-phase-b4-connection-firing-design.md`; plan - `docs/superpowers/plans/2026-06-11-f4-phase-b4-connection-firing.md`. -- **B5** horizontal conflict precedence — OUTSTANDING. - -**Evidence:** `apps/graphdb/src/graphdb_rules.erl` Phases A+B1+B2+B3 are -implemented. - -**Scope (Phases B–F):** - -- **§10 Composition rules** — class declares natural-constituent - component types and mandatory connections. Engine fires at - `create_instance` to propose or auto-create components. - -- **§9 Instantiation engine** — *guided* mode (one attribute at a - time, ontology constrains options) and *automatic* mode (values - derived from existing knowledge). Mode chosen by the ontology, not - the kernel. - -- **§11 Reactive learning:** - - *Naming-convention learning*: on attribute set, scan other AVPs on - the same instance for substring matches; encode detected pattern as - a class-level rule. - - *Connection-pattern learning*: on connection creation, record the - (source class, template, target class, connection type) tuple; - accumulate into connection rules. - - *Report-driven learning*: treat the firing report B2–B4 already emit - (the `proposed` / `auto` / `required` / `connected` / `not_connected` - outcomes) as a feedback signal. Observe which proposals a caller - accepts versus ignores, and which `required` connections get satisfied - after the fact, then feed the accumulated signal back into the rule - set — adjusting a rule's `mode` / `multiplicity`, or promoting a - recurring manually-made pattern into a new rule. - -- All rules stored as typed data in the ontology (kind = `class` with - an `is_rule = true` AVP, or a new `kind = rule` — same decision - point as templates faced). - -**Dependencies:** kernel pre-requisites (relationship kind, correct -inheritance, template support) have all landed. E1 is unblocked on the -kernel side. - ---- - -## Engineering Hygiene - -No blocking dependencies on any feature phase. Interleave at any point. +The canonical model is [`docs/TheKnowledgeNetwork.md`](docs/TheKnowledgeNetwork.md); +the current architecture is [`docs/Architecture.md`](docs/Architecture.md). --- -### L1. Rename `inherited_attributes/1` → `inherited_qcs/1` — RESOLVED (subsumed by L2) - -**Evidence:** `graphdb_class.erl:230-238, 638-651`. - -The function returns qualifying-characteristic *attribute nrefs* from -the class and its ancestors — not inherited *values*. The name -`inherited_attributes` implies §6 value inheritance, which is -different. - -**Fix:** rename to `inherited_qcs/1`. Reserve `inherited_attributes` -for §6 semantics if/when class-level bound-value inheritance is exposed -as its own API. - ---- - -### L2. Unify QC declarations and class-bound values into a single AVP shape — RESOLVED - -**Evidence:** `graphdb_class.erl:524-562, 863-908, 1001-1040`. -`do_add_qc` currently writes a sentinel-keyed AVP -`#{attribute => QcAttrNref, value => AttrNref}` to record that -`AttrNref` is a qualifying characteristic. Class-bound values (e.g. -`#{attribute => ColorAttrNref, value => red}`) share the same AVP list -but use a different key. `resolve_from_class` in `graphdb_instance` -must avoid confusing the two, and adding more concept tags in F4 would -make the list harder to reason about. - -**Fix:** replace the sentinel-keyed pattern with a unified shape: +## Rule Engine — completing the firing engine -- **QC declared, no bound value:** `#{attribute => AttrNref, value => undefined}` -- **QC with class-level bound value:** `#{attribute => AttrNref, value => SomeValue}` +The rules data model, the composition firing engine (mandatory / auto / +propose modes), and the connection firing engine (resolver-driven +mandatory / auto / propose) are implemented in `graphdb_rules` and +`graphdb_instance`. The remaining work is the rest of the engine that +consumes the rule data: conflict resolution, the interactive +instantiation modes, and reactive learning. The durable design contract +is `docs/designs/f4-graphdb-rules-design.md`. -Both forms are normal AVPs keyed by the actual attribute nref. Adding a -QC writes `undefined`; binding a class value updates the entry (or -writes it if not yet declared). `resolve_from_class` skips -`value = undefined` entries — they are schema declarations, not -resolved values. Inheritance walk collects all unique `attribute` keys -nearest-first, carrying `{AttrNref, Value | undefined}` pairs. +### Conflict precedence -**Changes required:** +When a class and its taxonomy ancestors each attach a rule that touches +the same component type or connection, the effective-rules gather +currently returns them additively, nearest-first, and resolves nothing. +Decide and implement horizontal conflict resolution: when two rules at +different levels genuinely conflict, which wins — does a nearer rule +shadow a farther one, or do they compose — and what the precedence order +is. This is the last outstanding piece of the firing engine's core; the +division is sketched in `docs/designs/f4-graphdb-rules-design.md` §11. -1. Remove the seeded `qualifying_characteristic` literal attribute and - `qc_attr_nref` from `graphdb_class` state — no longer needed. -2. `do_add_qc/3` writes `#{attribute => AttrNref, value => undefined}`. - Idempotent: if the key already exists (any value), leave it alone. -3. `inherited_attributes/1` → `inherited_qcs/1` (L1 rename, fold in - here). Return type changes from `[AttrNref]` to - `[{AttrNref, Value | undefined}]`, deduplicating by `AttrNref` with - nearest-ancestor priority. -4. `collect_all_qcs/2` and `collect_qc_nrefs/2` simplified to a fold - over all AVPs with dedup by `attribute` key. -5. `search_class_taxonomy` in `graphdb_instance.erl` — guard - `value =/= undefined` before treating an AVP as a resolved hit. +### Instantiation engine — guided and automatic modes -**Deferred:** instance-only enforcement (attributes that must never -receive a class-level value) belongs in the template attribute list, -which does not yet exist. The `undefined` shape accommodates this -naturally — an instance-only attribute stays `undefined` at every class -level. Enforcement is a follow-on task adjacent to L4/F4. +Spec §9. Two creation modes, chosen by the ontology rather than the +kernel: -**Note:** best done before F4 (E1) starts adding more concept tags to -class nodes. Subsumes L1 (`inherited_attributes/1` → `inherited_qcs/1`). +- **Guided** — the engine presents valid options one attribute at a + time, using ontology rules to constrain the choices at each step. +- **Automatic** — values are derived entirely from existing knowledge, + with no user interaction. ---- - -### L3. Single-row reads run inside `mnesia:transaction/1` — RESOLVED +### Reactive learning -**Evidence:** `graphdb_class.erl:506, 569-575, 601-611`, -`graphdb_instance.erl:393, 406, 453-459, 486, 499`, -`graphdb_mgr.erl:357`. +Spec §11. The ontology grows from observed use, not only from explicit +authoring: -**Fix:** use `mnesia:dirty_read/2` for read-only single-row lookups -that don't need transactional isolation. Reserve transactions for -multi-row writes and reads that must observe atomic state. +- **Naming-convention learning** — when an attribute value is set, scan + the other attribute-value pairs on the same instance for substring + matches; when the new value is composed of existing attribute values, + encode that pattern as a class-level rule so future instances populate + the attribute automatically. +- **Connection-pattern learning** — when a connection is made, record + the `(source class, template, target class, connection type)` tuple + and accumulate the observations into connection rules that guide + future connections of the same kind. +- **Report-driven learning** — treat the firing report the engine + already emits (the `proposed` / `auto` / `required` / `connected` / + `not_connected` outcomes) as a feedback signal. Observe which + proposals a caller accepts versus ignores, and which `required` + connections get satisfied after the fact, then feed the accumulated + signal back into the rule set — adjusting a rule's mode or + multiplicity, or promoting a recurring manually-made pattern into a + new rule. --- -### L4. Wire `graphdb_mgr` write-side to workers — RESOLVED +## Write-path completion -**Evidence:** `graphdb_mgr.erl:278-296`. `create_attribute`, -`create_class`, `create_instance`, `add_relationship` all return -`{error, not_implemented}` despite the workers being fully functional. +`graphdb_mgr` routes node and relationship creation to the workers. The +gaps that remain are node mutation, the template attribute list, and +wiring the multilingual write path. -The spec's organizing claim is that `graphdb_mgr` is the single public -entry point. Today that is true only for reads. *Higher impact than -others in this section — restores the spec's public API contract.* +### Node deletion and attribute-value-pair update -**Fix:** delegate each handler to the corresponding worker: -- `create_attribute` → `graphdb_attr:create_*` (route by kind) -- `create_class` → `graphdb_class:create_class/2` -- `create_instance` → `graphdb_instance:create_instance/3` -- `add_relationship` → `graphdb_instance:add_relationship/4` -- `delete_node`, `update_node_avps` → category guard, then - kind-appropriate worker. +`graphdb_mgr:delete_node/1` and `update_node_avps/2` still return +`{error, not_implemented}`: no worker implements node deletion or general +AVP update. Both already pass through the category guard (rejecting the +scaffold category nodes) before returning. Wire them to a worker once one +provides the underlying functionality. -**Design note — attribute categories per class context:** +### Template attribute list and instance-only enforcement -When wiring `create_class` and `update_node_avps`, the write-side must -account for two categories of attributes that a class declares: +A template currently carries only a name and its compositional arc into +the owning class — there is no per-template list of which attributes the +template scopes. Without it, the class write-side cannot distinguish two +categories of attribute a class declares: - **Class-bindable** — the class may supply a value (or a useful - default) for this attribute. Instances inherit the value and may - override it. Example: `num_wheels = 4` on a Car class. -- **Instance-only** — the class declares the attribute as relevant but - binding a value at the class level is a category error. The value is - meaningful only per-instance. Example: `serial_number`, `owner_name`. - Attempting to bind a class-level value for such an attribute should be - rejected or flagged. - -This distinction is **per-class, per-template context** — the same -attribute may be class-bindable in one class's template and instance-only -in another's. The enforcement point belongs in the template's attribute -declaration, not on the attribute node globally. - -The template attribute list does not yet exist (templates currently carry -only a name and their compositional arc). L4 implementation should treat -this as a known gap: wire the delegation first, then plan the template -attribute list and instance-only enforcement as a follow-on task (likely -adjacent to F4/E1, which adds rule-driven instantiation). Document the -gap in the Decision Log when L4 lands. - -#### Decision Log - -**L4 — `create_attribute` routing by ParentNref** (2026-05-18) - -`graphdb_mgr:create_attribute/3` routes to the appropriate -`graphdb_attr` worker function based on `ParentNref`: -- 6 / 9–12 (Names subtree) → `create_name_attribute/1` -- 7 (Literals) → `create_literal_attribute/2`; `type` extracted from `AVPs` map (default `string`) -- 8 / 13–16 (Relationships subtree) → `create_relationship_attribute/3` if both - `reciprocal_name` and `target_kind` present; `create_relationship_type/1` if neither; - `{error, {missing_avps, ...}}` if exactly one is present -- Unknown parent → `{error, {unknown_attribute_parent, Nref}}` - -`create_relationship_attribute/3` returns `{ok, {FwdNref, RevNref}}`; the mgr -normalises to `{ok, FwdNref}` (forward arc nref only). - -**L4 — Instance-only attribute enforcement deferred** (2026-05-18) - -The template attribute list (which would declare per-class, per-template whether an -attribute is class-bindable or instance-only) does not yet exist. `create_class` -and `update_node_avps` accept any AVP write without enforcement. This is a known -gap; enforcement is a follow-on task adjacent to F4/E1 (rule-driven instantiation). - -**L4 — `delete_node` and `update_node_avps` remain `not_implemented`** (2026-05-18) - -No worker currently implements node deletion or general AVP-update. Both operations -pass through the category guard (rejecting category nrefs 1–5) and then return -`{error, not_implemented}`. These will be wired when a worker adds the functionality. - ---- - -### L5. Relationship row IDs allocated from the global `nref_server` — **RESOLVED** (2026-05-19) - -New `rel_id_server` gen_server added to `apps/graphdb/src/` as first child of -`graphdb_sup`. All 23 `#relationship.id` allocations across 5 files migrated from -`nref_server:get_nref/0` to `rel_id_server:get_id/0`. Bootstrap test assertions -updated (nref floor now `>= 100002`; relationship IDs now start at 1). 4 CT tests added. - ---- - -### L6. Multi-Project Sessions — **DEFERRED** - -Added to Engineering Hygiene by the F4 design -(`docs/designs/f4-graphdb-rules-design.md` §9). The `Scope` argument on -every M6, F3, and F4 public API is the forward-compatibility hook: APIs -already accept `{project, _}`, but the gen_server handlers serve the -`environment` scope only and reject or empty `{project, _}` requests. - -L6 is the home for: - -1. Session state carrying `[{ProjectId, AnchorNref}]` (a list, not a - singleton). -2. Cross-project arc traversal — arcs whose target is a project nref - carry `target_kind` but not *which* project. -3. Session-level priority resolution (env, then project A's rules, then - project B's rules — or a declared order). -4. Project-scoped overlay tables for rule instances when project rules - are turned on. - -**Not blocked by, and does not block, F4 Phase A.** - ---- - -### L7. Literals subtree restructuring — **RESOLVED** (2026-05-25) - -Literals subtree (nref 7) partitioned by owning subsystem so each -worker seeds its literal attributes under a dedicated sub-group: - -- `Attribute Literals` — seeded by `graphdb_attr:init/1` (contains - `literal_type`, `target_kind`, `relationship_avp`, `attribute_type`) -- `Language Literals` — seeded by `graphdb_language:init/1` (contains - `base_language`, `project_language`) -- `Rule Literals` — seeded by `graphdb_rules:init/1` once F4 Phase A - lands - -`graphdb_attr:create_literal_attribute/3` arity added so callers can -specify a parent nref. `/2` retained as a delegating shim defaulting -to nref 7. - -Clean-slate seeding; no runtime migration code. - ---- - -### L8. Generalize `graphdb_attr` attribute placement — **RESOLVED** (2026-05-31) - -Parent nref is now a first-class, validated argument on every -`graphdb_attr` creator. Canonical general creators -`create_value_attribute/4` (single node) and -`create_relationship_attribute_pair/4` (reciprocal pair) back thin named -wrappers that preserve the default parents (6/7/8). `validate_parent/1` -rejects a non-existent or non-`attribute` parent before any write. -`create_relationship_attribute` renamed to -`create_relationship_attribute_pair`. Design at -`docs/designs/l8-graphdb-attr-placement-design.md`. Removes the F4 §10.1 -P1 placement blocker by construction. - ---- - -### L9. Non-instantiable (abstract) classes — **RESOLVED** (2026-06-01) - -A class may be designated non-instantiable (abstract) by an -`instantiable => false` marker AVP on the class node. `graphdb_attr` -seeds the `instantiable` boolean marker literal attribute in the -`Attribute Literals` sub-group. `graphdb_class:create_class/3` takes an -initial AVP list (`/2` delegates with `[]`); a class created with the -marker is born **without** a default template. `graphdb_class:is_instantiable/1` -reports the flag. `graphdb_instance:create_instance/3` **and** -`add_class_membership/2` refuse a non-instantiable class target with -`{error, {class_not_instantiable, ClassNref}}`. Permissive by default — -absence of the marker means instantiable. Design at -`docs/designs/l9-non-instantiable-classes-design.md`. Prerequisite for -F4 Phase A (Decision D15), which seeds the abstract `Rule` meta-class -root. - ---- - -### L10. Transaction observability for the allocate-outside-txn pattern — **OPEN** - -Every write-path worker (`graphdb_attr`, `graphdb_class`, -`graphdb_instance`, `graphdb_rules`) allocates node nrefs and -relationship row IDs **outside** the `mnesia:transaction/1` that writes -the rows, to keep the transaction fun free of side-effects on retry -(Mnesia may re-run the fun on lock conflict). The deliberate cost is -that an aborted transaction orphans the already-allocated nrefs/ids — -harmless given the unbounded monotonic nref space (no contiguity -invariant), but currently **unobservable**: nothing measures how often -the abort/retry path actually fires. - -Mnesia exposes node-global cumulative counters via -`mnesia:system_info/1` that make the real rate measurable: - -- `transaction_restarts` — fun re-runs (lock-conflict / deadlock retries) -- `transaction_failures` — aborts -- `transaction_commits` — successful commits -- `transaction_log_writes` - -These are node-global and cumulative (no reset, no per-table or -per-callsite attribution), so a delta-snapshot helper is needed to make -them useful for ad-hoc workload observation. - -Scope: - -1. Add a `dev_lib` helper that snapshots the four counters around a fun - and returns the deltas (commits / failures / restarts / log writes), - for ad-hoc observability during review and load checks. -2. Decide whether the rule/attr/instance write paths warrant their own - `{atomic,_}` vs `{aborted,_}` counters for per-callsite leak-rate - attribution, or whether the global counters suffice. Document the - decision. -3. Confirm the allocate-outside-txn rationale carries an inline comment - at each allocation site (present in `graphdb_attr` / `graphdb_instance`; - verify `graphdb_class` and `graphdb_rules`). - -Observability-only; no behavioural change to the write paths. - ---- - -### Task 7. Wire `dictionary_server` and `term_server` to `dictionary_imp` — **RESOLVED** (2026-05-19) - -Both gen_servers delegate to `dictionary_imp` via `start_dictionary/stop_dictionary` -in `init/terminate` and forward all CRUD calls. Also fixed a pre-existing one-line bug -in `dictionary_imp:delete/2` (wrong ETS key type). 14 CT tests added (7 per server). - ---- - -### Task 8. Scaffold nref constants → shared `graphdb_nrefs.hrl` header — **RESOLVED** (2026-05-20) - -`apps/graphdb/include/graphdb_nrefs.hrl` introduced with 36 named macros covering -scaffold nrefs 1–35 (`NREF_*`, `NAME_ATTR_*`, `ARC_*`) and the permanent English -instance nref 10000 (`NREF_ENGLISH`). All inline `-define` blocks removed from five -source files (`graphdb_attr`, `graphdb_class`, `graphdb_instance`, `graphdb_language`, -`graphdb_mgr`); all raw integers 17–35 and 10000 replaced with macros in seven test -files. Companion `graphdb_nrefs.erl` exports `scaffold_spec/0` and `verify/0`; verify -is called at the end of `graphdb_bootstrap:do_load/0` as a fatal congruency check. -`graphdb_bootstrap` module is deleted+purged from the code server in `graphdb_mgr:init/1` -after successful load. 2 CT tests in `graphdb_nrefs_SUITE`. 320 tests (217 CT + -103 EUnit), all green, zero warnings. - ---- - -### E2. Non-normal OTP start types — **RESOLVED** (2026-05-21) - -`seerstone:start/2` and `nref:start/2` now delegate `{takeover, Node}` and -`{failover, Node}` to the normal start path rather than hitting `?NYI`. -Full distributed takeover/failover semantics deferred until a distributed -deployment is planned. - ---- - -### E3. `code_change/3` — hot code upgrades — **DEFERRED** - -NYI in all gen_server modules: `nref_allocator`, `nref_server`, all six -`graphdb_*` workers. Implement when the first hot-upgrade deployment is -planned. Premature until there is a versioned release to upgrade in place. - ---- - -### E4. `start_phases` / `start_phase/3` — **DEFERRED** - -No `.app.src` file defines `start_phases`, so `start_phase/3` is never -called. Revisit when an externally-visible entry point (API server, socket + default); instances inherit it and may override. *Example: + `num_wheels = 4` on a Car class.* +- **Instance-only** — the class declares the attribute as relevant, but + binding a value at the class level is a category error; the value is + meaningful only per instance. *Example: `serial_number`, + `owner_name`.* Binding a class-level value for such an attribute should + be rejected. + +The distinction is per-class, per-template — the same attribute may be +class-bindable in one class's template and instance-only in another's. +Build the template attribute list, then enforce instance-only rejection +in `create_class` and `update_node_avps`. The unified qualifying- +characteristic AVP shape (a declared-but-unbound attribute carries +`value => undefined`) already accommodates an instance-only attribute +naturally — it stays `undefined` at every class level. + +### Multilingual write-path integration + +Now unblocked — the `graphdb_mgr` write-side is wired. When an +environment node is created, the write path must additionally: + +1. Create the node atomically in one Mnesia transaction. +2. Post-commit and outside that transaction (best-effort), call every + registered translation hook with the new nref and its English AVPs. +3. If a session language list is supplied with labels, call + `set_labels/3` for each language. + +Steps 2–3 are deliberately not atomically coupled to step 1: a failed +hook or a missing language label does not roll back node creation. Do not +auto-duplicate environment labels into dialect overlay tables — a dialect +override is an explicit authoring decision, never inferred. Project- +instance label writes depend on the multi-project work below (project- +scoped overlay tables). + +--- + +## Multi-project sessions + +Every public API already accepts a `Scope` of `environment | {project, +_}`; the handlers serve the `environment` scope only and reject or empty +`{project, _}` requests. This area turns project scope on: + +- Session state carrying a list of `{ProjectId, AnchorNref}` (a list, + not a singleton). +- Cross-project arc traversal — an arc whose target is a project nref + carries `target_kind` but not *which* project; the session must supply + that context. +- Session-level priority resolution — environment first, then project + A's rules, then project B's, or a declared order. +- Project-scoped overlay tables for rule instances and for language + labels (`language__`). + +**Open question — multi-class instance creation.** `create_instance` +stays single-class: one primary driving class. Additional class +memberships are expressed as rules *on* the primary class. The +load-bearing question is whether the effective-rules gather should +recurse transitively into a conferred class's rules — which reframes +multi-class creation from an API-signature problem into a gather- +transitivity problem. A class-list / signature-widening framing was +considered and rejected; see `docs/designs/f4-phase-b4-connection-firing-design.md` §7. + +--- + +## Operational and lifecycle + +No feature dependencies; interleave at any point. + +### Transaction observability + +Every write-path worker allocates node nrefs and relationship row IDs +*outside* the `mnesia:transaction/1` that writes the rows, so the +transaction fun stays free of side-effects when Mnesia re-runs it on a +lock conflict. The deliberate cost is that an aborted transaction orphans +the already-allocated ids — harmless given the unbounded monotonic nref +space, but currently unmeasured. + +Add a development helper that snapshots Mnesia's cumulative counters +(`transaction_restarts`, `transaction_failures`, `transaction_commits`, +`transaction_log_writes` via `mnesia:system_info/1`) around a fun and +returns the deltas. Decide whether the write paths warrant their own +per-callsite `{atomic, _}` / `{aborted, _}` counters or whether the +global counters suffice, and document the decision. Confirm the +allocate-outside-transaction rationale carries an inline comment at each +allocation site. Observability only — no behavioural change to the write +paths. + +### Hot code upgrade — `code_change/3` *(deferred)* + +`code_change/3` is unimplemented in every gen_server (`nref_allocator`, +`nref_server`, and all `graphdb_*` workers). Implement when the first +hot-upgrade deployment is planned — premature until there is a versioned +release to upgrade in place. + +### Phased application startup *(deferred)* + +No `.app.src` defines `start_phases`, so `start_phase/3` is never called. +Revisit when an externally-visible entry point (an API server or socket listener) is added to `seerstone` that must not accept connections until -the full graphdb stack is bootstrapped — at that point phased startup -becomes necessary to close the window between port-open and data-ready. - ---- - -### E5. Replace `included_applications` with peer-app dependencies — **RESOLVED** (2026-05-21) - -**Evidence:** `apps/database/src/database.app.src` declares -`included_applications: [graphdb, dictionary]`. This is Dallas's 2008 -OTP idiom; modern OTP discourages it because included apps lose -independent restart, code reload, and application-callback semantics. -The `seerstone`↔`database` boundary was already modernized (2026-05-09); -this applies the same treatment one level deeper. - -**Fix:** - -1. Remove `included_applications: [graphdb, dictionary]` from - `database.app.src`. Add `graphdb` and `dictionary` to a higher-level - `applications:` dependency list. -2. Drop `graphdb_sup` and `dictionary_sup` from `database_sup:init/1`. -3. Decide whether `database` itself remains an OTP application. -4. Update `ARCHITECTURE.md` §5 and supervision-tree diagrams in - `CLAUDE.md` files. - -**Note:** best done before E3 and E2, since `included_applications` -complicates both hot upgrades and distributed-app semantics. +the full graphdb stack is bootstrapped — phased startup then closes the +window between port-open and data-ready. diff --git a/apps/graphdb/CLAUDE.md b/apps/graphdb/CLAUDE.md index 2890edc..c199827 100644 --- a/apps/graphdb/CLAUDE.md +++ b/apps/graphdb/CLAUDE.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later ## Purpose -`graphdb` is the core **graph database** OTP application within the SeerStone system. It is a peer OTP application started by `application_master` after `mnesia` and `nref`, and manages graph data through `graphdb_sup` and six worker gen_servers. The data model is the knowledge graph described in `the-knowledge-network.md` (US patents 5,379,366; 5,594,837; 5,878,406 — Noyes). +`graphdb` is the core **graph database** OTP application within the SeerStone system. It is a peer OTP application started by `application_master` after `mnesia` and `nref`, and manages graph data through `graphdb_sup` and six worker gen_servers. The data model is the knowledge graph described in `../../docs/TheKnowledgeNetwork.md` (US patents 5,379,366; 5,594,837; 5,878,406 — Noyes). ## Files @@ -137,7 +137,7 @@ Every graph node is stored as a Mnesia record: ``` `parents` and `classes` are caches of the authoritative arcs in the -`relationships` table. See ARCHITECTURE.md §3 for the cache invariant +`relationships` table. See `../../docs/Architecture.md` §3 for the cache invariant and the `graphdb_mgr:verify_caches/0` / `rebuild_caches/0` audit APIs. Downward queries ("children of X") read outgoing arcs from `relationships` filtered by kind + characterization. @@ -357,7 +357,7 @@ firing engine (Phase B5 precedence + Phases C–F) remains, tracked in `TASKS.md - `graphdb_sup:start_link/0` takes no args, matching every supervisor in the umbrella. It is called from `graphdb:start/2` after `graphdb_nref:set_permanent_phase/0` arms the permanent-tier allocator. `graphdb:start/2` then calls `graphdb_nref:set_runtime_phase/0` after `start_link` returns. - `graphdb_bootstrap`, `graphdb_mgr` (startup + read API), `graphdb_attr`, `graphdb_class`, `graphdb_instance`, `graphdb_language`, `graphdb_query`, and `graphdb_rules` (F4 A+B1+B2+B3+B4) are implemented. Remaining work is in `TASKS.md` at the project root. -- Consult `the-knowledge-network.md` for the full model spec before implementing +- Consult `../../docs/TheKnowledgeNetwork.md` for the full model spec before implementing ## Compile diff --git a/apps/graphdb/src/graphdb_attr.erl b/apps/graphdb/src/graphdb_attr.erl index bb1c1bc..b741641 100644 --- a/apps/graphdb/src/graphdb_attr.erl +++ b/apps/graphdb/src/graphdb_attr.erl @@ -105,8 +105,8 @@ literal_type_nref, %% integer() -- seeded literal attribute target_kind_nref, %% integer() -- seeded literal attribute relationship_avp_nref, %% integer() -- seeded literal attribute - attribute_type_nref, %% integer() -- seeded literal attribute (M8) - instantiable_nref %% integer() -- seeded marker literal attribute (L9) + attribute_type_nref, %% integer() -- seeded literal attribute + instantiable_nref %% integer() -- seeded marker literal attribute }). diff --git a/apps/graphdb/src/graphdb_class.erl b/apps/graphdb/src/graphdb_class.erl index a72749a..651746d 100644 --- a/apps/graphdb/src/graphdb_class.erl +++ b/apps/graphdb/src/graphdb_class.erl @@ -29,7 +29,7 @@ %% Initial implementation: taxonomic hierarchy over Mnesia. Provides %% create_class/2, add_qualifying_characteristic/2, get_class/1, %% subclasses/1, ancestors/1, inherited_qcs/1. -%% L2: unified QC AVP shape — value=>undefined for declarations. +%% Unified QC AVP shape — value=>undefined for declarations. %%--------------------------------------------------------------------- -module(graphdb_class). -behaviour(gen_server). @@ -95,7 +95,7 @@ -record(state, { instantiable_nref %% integer() -- seeded `instantiable` marker, cached - %% from graphdb_attr at init (L9) + %% from graphdb_attr at init }). @@ -555,7 +555,7 @@ template_rows(ClassNref, AVPs, InstAttr) -> %% %% Returns true when AVPs contains #{attribute => InstAttr, value => false}. %% Deliberately duplicated in graphdb_instance (the two workers share no -%% module); L9 avoids a shared util for one predicate (YAGNI). +%% module); this avoids a shared util for one predicate (YAGNI). %%----------------------------------------------------------------------------- is_marked_non_instantiable(AVPs, InstAttr) -> lists:any(fun diff --git a/apps/graphdb/src/graphdb_instance.erl b/apps/graphdb/src/graphdb_instance.erl index 6bc5ce0..df4f96b 100644 --- a/apps/graphdb/src/graphdb_instance.erl +++ b/apps/graphdb/src/graphdb_instance.erl @@ -76,7 +76,7 @@ %% Constants %%--------------------------------------------------------------------- %% Rules live in the shared ontology; project-scoped rules are not yet -%% supported (B1/B2). Firing always consults environment-scope rules. +%% supported. Firing always consults environment-scope rules. -define(RULE_SCOPE, environment). %%--------------------------------------------------------------------- @@ -104,8 +104,8 @@ target_kind_avp_nref, %% integer() -- nref of the seeded `target_kind` %% literal-attribute, cached from graphdb_attr %% at init time and used by add_relationship - %% validation (M3). - instantiable_nref %% integer() -- seeded `instantiable` marker (L9) + %% validation. + instantiable_nref %% integer() -- seeded `instantiable` marker }). @@ -188,7 +188,7 @@ create_instance(Name, ClassNref, ParentNref) -> %% create_instance(Name, ClassNref, ParentNref, Resolver) -> %% {ok, Nref, report()} | {error, Reason, report()} | {error, Reason} %% -%% As /3, but threads a connection Resolver (B4). /3 uses the built-in +%% As /3, but threads a connection Resolver. /3 uses the built-in %% report_only resolver (defer-all): every connection rule surfaces as a report %% outcome and nothing is connected. %%----------------------------------------------------------------------------- @@ -197,7 +197,7 @@ create_instance(Name, ClassNref, ParentNref, Resolver) gen_server:call(?MODULE, {create_instance, Name, ClassNref, ParentNref, Resolver}). -%% report_only(ConnContext) -> defer (the built-in /3 resolver, B4-D2) +%% report_only(ConnContext) -> defer (the built-in /3 resolver) report_only(_Ctx) -> defer. @@ -244,7 +244,7 @@ add_relationship(SourceNref, CharNref, TargetNref, ReciprocalNref, %% add_relationship(SourceNref, CharNref, TargetNref, ReciprocalNref, %% TemplateNref, {FwdAVPs, RevAVPs}) -> ok | {error, term()} %% -%% Full form (M5): callers can stamp per-direction metadata AVPs on the +%% Full form: callers can stamp per-direction metadata AVPs on the %% two connection rows. AVPs are asymmetric -- forward and reverse are %% specified independently, since §5 says metadata such as provenance, %% confidence, weights, and validity windows is per-direction. @@ -357,9 +357,9 @@ resolve_value(InstanceNref, AttrNref) -> init([]) -> logger:info("graphdb_instance: started"), %% Cache the seeded `target_kind` literal-attribute nref from - %% graphdb_attr. Used by add_relationship validation (M3) to check + %% graphdb_attr. Used by add_relationship validation to check %% that an arc's target node has the kind declared on the - %% characterization. Also cache `instantiable` (L9) to check at + %% characterization. Also cache `instantiable` to check at %% create_instance time that the class is not marked non-instantiable. %% graphdb_attr is started before graphdb_instance by graphdb_sup, %% so this call is safe at init time. @@ -464,10 +464,10 @@ find_avp_value([_ | Rest], AttrNref) -> %% do_create_instance(Name, ClassNref, ParentNref, Ctx) %% -> {ok, Nref, report()} | {error, Reason, report()} | {error, Reason} %% -%% The unifying internal entry (B2-D2): every cascade level flows through +%% The unifying internal entry: every cascade level flows through %% here, never the gen_server API (that would deadlock). Ctx carries %% inst_attr, on_path (the class path for the on-path cycle guard), resolver, -%% and the stable root_parent / root_source anchors (B4-D2a). Validates the +%% and the stable root_parent / root_source anchors. Validates the %% class (must be kind=class and instantiable) and parent (must exist); %% pre-PLAN errors return a 2-tuple {error, Reason} (no report). Post-PLAN %% paths return 3-tuples. @@ -490,7 +490,7 @@ do_create_instance(Name, ClassNref, ParentNref, Ctx) -> %% fire_create(Name, ClassNref, ParentNref, Ctx) %% -> {ok, Nref, report()} | {error, Reason, report()} %% -%% PLAN → EXECUTE → POST-COMMIT (B2-D1/D2/D3). Calls graphdb_rules for the +%% PLAN → EXECUTE → POST-COMMIT. Calls graphdb_rules for the %% abstract plan tree, then executes the mandatory subtree atomically, then %% fires auto children best-effort post-commit. %%----------------------------------------------------------------------------- @@ -522,7 +522,7 @@ fire_create(Name, ClassNref, ParentNref, Ctx) -> %% %% At the top level root_source is undefined -> bind it to the freshly %% allocated root nref; for a threaded descendant it is already set and -%% kept unchanged (B4-D2a). +%% kept unchanged. %%----------------------------------------------------------------------------- bind_root_source(Ctx, RootNref) -> case maps:get(root_source, Ctx) of @@ -536,7 +536,7 @@ bind_root_source(Ctx, RootNref) -> %% | {error, Reason, report()} %% %% Allocates every node's nrefs/ids OUTSIDE the transaction, RESOLVEs the -%% connection rules for the mandatory subtree (B4), then writes the root, the +%% connection rules for the mandatory subtree, then writes the root, the %% whole mandatory composition subtree, and any committed mandatory connection %% rows in ONE Mnesia transaction. Returns the InstPlan plus the AutoConnPlan %% (the post-commit auto-connection write list — empty in this task). @@ -574,7 +574,7 @@ execute(RootName, _RootClass, RootParent, Ctx, PlanTree) -> end. %%============================================================================= -%% Connection Firing -- RESOLVE (B4) +%% Connection Firing -- RESOLVE %%============================================================================= %%----------------------------------------------------------------------------- @@ -618,7 +618,7 @@ resolve_rules([{Rule, Deploy, Spec} | Rest], SourceNref, Ctx, Acc) -> case Mode of propose -> %% propose connection rules are advisory: surface `proposed`, never - %% consult the resolver, never connect (mirrors B3 propose). + %% consult the resolver, never connect (mirrors propose mode). Acc1 = add_conn_outcome(Acc, Rule, Deploy, conn_outcome_base(SourceNref, Spec, proposed)), resolve_rules(Rest, SourceNref, Ctx, Acc1); @@ -644,7 +644,7 @@ resolve_rules([{Rule, Deploy, Spec} | Rest], SourceNref, Ctx, Acc) -> %% -> {ok, Acc'} | {error, Reason, Report} %% %% Validates the resolver-returned targets, applies the {Min, Max} range, and -%% routes by mode. In B4 Task 5 only `mandatory` is implemented (validate + +%% routes by mode. So far only `mandatory` is implemented (validate + %% abort, build root-txn rows, tentative `connected` outcomes); `auto` is added %% in a later task. %%----------------------------------------------------------------------------- @@ -679,7 +679,7 @@ connect_targets(auto, List, Rule, Deploy, Spec, SourceNref, Rest, Ctx, {Rows, Auto, Rep}) -> TClass = maps:get(target_class, Spec), {Valid, Invalid} = split_valid(List, TClass, SourceNref), - %% auto does NOT enforce the floor (B4-D5) -- Min is ignored; only Max caps. + %% auto does NOT enforce the floor -- Min is ignored; only Max caps. {_Min, Max} = maps:get(multiplicity, Deploy, {1, 1}), ToConnect = cap(Valid, Max), Char = maps:get(characterization, Spec), @@ -751,7 +751,7 @@ mandatory_rows(Targets, SourceNref, Spec, Template) -> %% conn_fail(Reason, CulpritRule, Spec, RepAcc) -> Report %% Mandatory-connection abort report: every already-emitted connection outcome %% becomes not_attempted; the culprit gets one `failed` carrying its connection -%% keys (so the rollback cause is discriminable by rule kind, B4-D7). +%% keys (so the rollback cause is discriminable by rule kind). conn_fail(Reason, CulpritRule, Spec, RepAcc) -> NA = [ RR#{outcomes => [#{index => 1, status => not_attempted} || _ <- Os]} @@ -767,7 +767,7 @@ conn_fail(Reason, CulpritRule, Spec, RepAcc) -> %% Target is a bare nref or {Nref, {Fwd, Rev}}. Valid iff the nref exists, is a %% kind=instance node, and is an instance of TargetClass or a subclass of it. %% No self-check is needed: the source is uncommitted at RESOLVE, so a readable -%% instance is necessarily distinct from it (B4-D6). +%% instance is necessarily distinct from it. %%----------------------------------------------------------------------------- validate_target(Target, TargetClass, _SourceNref) -> Nref = target_nref(Target), @@ -791,7 +791,7 @@ target_nref({Nref, {_F, _R}}) when is_integer(Nref) -> Nref. target_avps(Nref) when is_integer(Nref) -> {[], []}; target_avps({_Nref, {Fwd, Rev}}) -> {Fwd, Rev}. -%% conn_context(Rule, Deploy, Spec, SourceNref, Ctx) -> ConnContext (B4-D2/D2a) +%% conn_context(Rule, Deploy, Spec, SourceNref, Ctx) -> ConnContext conn_context(Rule, Deploy, Spec, SourceNref, Ctx) -> #{rule => Rule, characterization => maps:get(characterization, Spec), @@ -818,10 +818,10 @@ add_conn_outcome({Rows, Auto, Rep}, Rule, Deploy, Outcome) -> %%----------------------------------------------------------------------------- %% fire_connections(AutoConnPlan) -> report() %% -%% POST-COMMIT best-effort writer for `auto` connections (B4). Writes each +%% POST-COMMIT best-effort writer for `auto` connections. Writes each %% queued auto connection in its own transaction; a successful write is a %% `connected` outcome, a write failure is a `failed` outcome that never rolls -%% the instance back (B4-D4/D7). An empty plan yields an empty report. +%% the instance back. An empty plan yields an empty report. %%----------------------------------------------------------------------------- fire_connections(AutoConnPlan) -> lists:foldl(fun fire_auto_connection/2, [], AutoConnPlan). @@ -962,7 +962,7 @@ push_on_path(Ctx, ClassNref) -> %% fire_one_auto(RuleNode, Deploy, OwnerNref, Ctx, Acc) -> report() %% %% Check order: instantiable, then the vertical-cycle cut, then expansion. -%% mints Min children post-commit (B-prep). Ctx's on_path already carries the +%% mints Min children post-commit. Ctx's on_path already carries the %% owner's class (pushed by fire_auto). %%----------------------------------------------------------------------------- fire_one_auto(RuleNode, Deploy, OwnerNref, Ctx, Acc) -> @@ -975,7 +975,7 @@ fire_one_auto(RuleNode, Deploy, OwnerNref, Ctx, Acc) -> _ -> %% true (or {error,_} -> treated as fireable; create reports) {Min, _Max} = maps:get(multiplicity, Deploy, {1, 1}), case lists:member(ChildClass, maps:get(on_path, Ctx)) of - true -> Acc; %% vertical cycle cut (B2-D5) + true -> Acc; %% vertical cycle cut false -> fire_auto_children(RuleNode, Deploy, ChildClass, Min, 1, OwnerNref, Ctx, Acc) end @@ -1016,7 +1016,7 @@ fire_auto_children(RuleNode, Deploy, ChildClass, Mult, I, OwnerNref, Ctx, %%----------------------------------------------------------------------------- %% fire_propose(InstPlan, OnPath) -> report() %% -%% POST-COMMIT, side-effect-free (B3). Walks the instantiated plan tree +%% POST-COMMIT, side-effect-free. Walks the instantiated plan tree %% (root + mandatory descendants) and surfaces each node's propose_rules as %% `proposed` outcomes. Materialises NOTHING — a proposal is a suggestion the %% caller may accept by calling create_instance/3 for the proposed_class @@ -1024,7 +1024,7 @@ fire_auto_children(RuleNode, Deploy, ChildClass, Mult, I, OwnerNref, Ctx, %% InstPlan; their propose rules surface via their own do_create_instance %% sub-report. Mirrors fire_auto/2's traversal. %% -%% B3 OI-B3-5 (shallow): no recursion into proposed children — nothing is +%% Shallow: no recursion into proposed children — nothing is %% created, so there is nothing to recurse into. A future propose-with-options %% feature may supersede this. %%----------------------------------------------------------------------------- @@ -1043,18 +1043,18 @@ fire_propose(#{nref := Nref, class := Class, propose_rules := Props, %% fire_one_propose(RuleNode, Deploy, OwnerNref, OnPath1, Acc) -> report() %% %% Emits `proposed` outcome(s) for one propose rule. No instantiability -%% guard (B3 design §3.2): a proposal creates nothing, so an abstract target +%% guard (propose-mode design §3.2): a proposal creates nothing, so an abstract target %% cannot break anything; the caller validates on accept. %%----------------------------------------------------------------------------- fire_one_propose(RuleNode, Deploy, OwnerNref, OnPath1, Acc) -> ChildClass = graphdb_rules:rule_child_class(RuleNode), - %% B3 OI-B3-2: on-path cycle cut — do not propose a class already on the - %% root->here path (mirrors B2-D5). Supersedable by propose-with-options. + %% On-path cycle cut — do not propose a class already on the + %% root->here path. Supersedable by propose-with-options. case lists:member(ChildClass, OnPath1) of true -> Acc; false -> - %% B-prep: propose surfaces Min proposed outcomes, each carrying + %% Propose surfaces Min proposed outcomes, each carrying %% max => Max so the report keeps the open-ended ceiling (Max may %% be `unbounded'). Generalises the old index=unbounded sentinel. {Min, Max} = maps:get(multiplicity, Deploy, {1, 1}), @@ -1104,7 +1104,7 @@ do_validate_class(ClassNref, InstAttr) -> %% Returns true only when the AVP list contains an entry %% #{attribute => InstAttr, value => false}. Absence = permissive. %% The duplication of this helper in graphdb_class is intentional — the -%% two workers do not share a module, and L9 deliberately does NOT +%% two workers do not share a module, and this code deliberately does NOT %% introduce a shared util module for one small predicate (YAGNI). is_marked_non_instantiable(AVPs, InstAttr) -> lists:any(fun @@ -1130,7 +1130,7 @@ do_validate_parent(ParentNref) -> %% TemplateSpec, State) -> ok | {error, term()} %% %% TemplateSpec is either the atom `default` (look up source's class -%% default template) or an integer template nref. M3 validation +%% default template) or an integer template nref. Arc validation %% (existence + arc-label kind + target_kind agreement) runs first, %% then class lookup, template resolution, scope check, and the %% two-row write of the connection arcs with the Template AVP stamped @@ -1168,7 +1168,7 @@ do_add_relationship(SourceNref, CharNref, TargetNref, ReciprocalNref, %% validate_arc_endpoints(Source, Char, Target, Reciprocal, TkAttr) -> %% ok | {error, term()} %% -%% M3 validation. Reads all four nodes inside one mnesia transaction +%% Arc validation. Reads all four nodes inside one mnesia transaction %% and rejects: %% - missing source / target / characterization / reciprocal %% - characterization or reciprocal that is not kind=attribute @@ -1281,8 +1281,8 @@ validate_template_scope(TemplateNref, SourceClass, TargetClass) -> %% -> [{relationships, #relationship{}}] %% %% Builds the two directed connection rows (Template AVP at index 0). Rel-ids -%% are allocated here, OUTSIDE any transaction (L10). No write -- the caller -%% decides which transaction the rows land in (B4 mandatory connections ride the +%% are allocated here, OUTSIDE any transaction. No write -- the caller +%% decides which transaction the rows land in (mandatory connections ride the %% composition root txn; auto connections are written post-commit). %%----------------------------------------------------------------------------- build_connection_rows(SourceNref, CharNref, TargetNref, ReciprocalNref, @@ -1646,7 +1646,7 @@ resolve_from_ancestors(ParentNref, AttrNref) -> %% head_parent(Parents) -> integer() | undefined %% %% Returns the first parent in the cache list, or `undefined` for root -%% nodes (empty parents list). Used by single-chain ancestor walks; H3 +%% nodes (empty parents list). Used by single-chain ancestor walks %% will introduce multi-parent walks that traverse the full list. %%----------------------------------------------------------------------------- head_parent([]) -> undefined; @@ -1719,7 +1719,7 @@ search_targets([Nref | Rest], AttrNref) -> %%============================================================================= -%% Firing Report Helpers (B2-D6) +%% Firing Report Helpers %%============================================================================= %% summarize/1 is exported in TEST builds and available for external callers diff --git a/apps/graphdb/src/graphdb_language.erl b/apps/graphdb/src/graphdb_language.erl index 8572f10..f3ec186 100644 --- a/apps/graphdb/src/graphdb_language.erl +++ b/apps/graphdb/src/graphdb_language.erl @@ -17,7 +17,7 @@ %% Stub implementation. %%--------------------------------------------------------------------- %% Rev A Date: 2026 Author: David W. Thomas -%% M6 multilingual layer implementation. +%% Multilingual layer implementation. %%--------------------------------------------------------------------- -module(graphdb_language). -behaviour(gen_server). diff --git a/apps/graphdb/src/graphdb_mgr.erl b/apps/graphdb/src/graphdb_mgr.erl index 0ff15cc..947cb5f 100644 --- a/apps/graphdb/src/graphdb_mgr.erl +++ b/apps/graphdb/src/graphdb_mgr.erl @@ -25,7 +25,7 @@ %% {error, not_implemented} until those workers are implemented. %%--------------------------------------------------------------------- %% Rev B Date: May 2026 Author: (completion of Dallas Noyes's design) -%% L4: wire write-side delegation -- create_attribute routes to +%% Write-side delegation -- create_attribute routes to %% graphdb_attr by ParentNref subtree; create_class and create_instance %% delegate directly to graphdb_class and graphdb_instance respectively; %% add_relationship delegates to graphdb_instance. delete_node and diff --git a/apps/graphdb/src/graphdb_query.erl b/apps/graphdb/src/graphdb_query.erl index a35d233..552c068 100644 --- a/apps/graphdb/src/graphdb_query.erl +++ b/apps/graphdb/src/graphdb_query.erl @@ -9,34 +9,33 @@ %% maintains snapshot-semantics sessions with a %% read-through cache. %% -%% F3 sequencing: session API (new_session/0, refresh/1) -%% is real. Q1 (#q_get_node{}), Q1b (#q_get_arcs{}), Q2 -%% (#q_describe{} for kind=attribute), Q3 (#q_describe{} -%% for kind=class), Q4 (#q_describe{} for kind=instance), -%% Q5 (#q_instances_of{}), and Q6 (#q_find_path{}) are +%% Query sequencing: session API (new_session/0, refresh/1) +%% is real. #q_get_node{}, #q_get_arcs{}, #q_describe{} +%% (for kind=attribute, class, or instance), +%% #q_instances_of{}, and #q_find_path{} are %% implemented along with resume/2 and snapshot_expired -%% detection. F3 walking skeleton complete. +%% detection. Walking skeleton complete. %% %% Design source: docs/designs/f3-graphdb-query-design.md. %%--------------------------------------------------------------------- %% Revision History %%--------------------------------------------------------------------- %% Rev A Date: May 2026 Author: David W. Thomas -%% Initial skeleton implementation (F3 Task 2). +%% Initial skeleton implementation. %% Rev A.1 Date: May 2026 Author: David W. Thomas -%% Q1 (#q_get_node{}) implemented (F3 Task 3). +%% #q_get_node{} implemented. %% Rev A.2 Date: May 2026 Author: David W. Thomas -%% Q1b (#q_get_arcs{}) implemented (F3 Task 4). +%% #q_get_arcs{} implemented. %% Rev A.3 Date: May 2026 Author: David W. Thomas -%% Q2 (#q_describe{} for kind=attribute) implemented (F3 Task 5). +%% #q_describe{} for kind=attribute implemented. %% Rev A.4 Date: May 2026 Author: David W. Thomas -%% Q3 (#q_describe{} for kind=class) implemented (F3 Task 6). +%% #q_describe{} for kind=class implemented. %% Rev A.5 Date: May 2026 Author: David W. Thomas -%% Q4 (#q_describe{} for kind=instance) implemented (F3 Task 7). +%% #q_describe{} for kind=instance implemented. %% Rev A.6 Date: May 2026 Author: David W. Thomas -%% Q5 (#q_instances_of{}) implemented (F3 Task 8). +%% #q_instances_of{} implemented. %% Rev A.7 Date: May 2026 Author: David W. Thomas -%% Q6 (#q_find_path{}) + resume/2 + snapshot_expired (F3 Task 9). +%% #q_find_path{} + resume/2 + snapshot_expired. %%--------------------------------------------------------------------- -module(graphdb_query). -behaviour(gen_server). @@ -71,7 +70,7 @@ %%--------------------------------------------------------------------- -%% Records — mirror canonical shapes (see ARCHITECTURE.md §3). +%% Records — mirror canonical shapes (see docs/Architecture.md §3). %% Defined locally so this module compiles standalone; matches the %% pattern used in graphdb_language, graphdb_class, graphdb_instance. %%--------------------------------------------------------------------- @@ -150,7 +149,7 @@ execute_query(Query, Session) when is_map(Session) -> resume(Cont, Session) when is_map(Session) -> gen_server:call(?MODULE, {resume, Cont, Session}). -%% find_path/3 — public convenience matching the F3 task spec API. +%% find_path/3 — public convenience matching the query task spec API. find_path(From, To, MaxDepth) -> execute_query(#q_find_path{from = From, to = To, @@ -345,7 +344,7 @@ arc_to_map(#relationship{id = Id, %% describe_attribute(Node, LangSpec, Session) %% -> {{ok, ResultMap}, Session1} %% -%% Q2: composes the read-through node lookup with downward-arc traversal +%% describe(attribute): composes the read-through node lookup with downward-arc traversal %% to enumerate taxonomy children, then resolves a label for self + %% parent + each child via graphdb_language. Returns a map with %% nref/kind/attribute_type/parent/children/avps/labels. @@ -383,7 +382,7 @@ describe_attribute(#node{nref = N, parents = Parents, %% describe_class(Node, LangSpec, Session) %% -> {{ok, ResultMap}, Session1} %% -%% Q3: superclasses come from the parents cache; ancestors come from +%% describe(class): superclasses come from the parents cache; ancestors come from %% graphdb_class:ancestors/1 (multi-parent DAG walk); subclasses come %% from graphdb_class:subclasses/1. QCs are returned as the flat %% [{AttrNref, Value}] list produced by graphdb_class:inherited_qcs/1 @@ -416,7 +415,7 @@ describe_class(#node{nref = N, parents = Parents, %% describe_instance(Node, LangSpec, Session) %% -> {{ok, ResultMap}, Session1} %% -%% Q4: surfaces compositional + class structure, resolved attributes +%% describe(instance): surfaces compositional + class structure, resolved attributes %% via 4-priority inheritance (Task 0's resolve_value/2 returns %% {ok, Value, Source}), and BOTH outgoing and incoming connection %% arcs (per-direction characterization and AVPs differ). diff --git a/apps/graphdb/src/graphdb_rules.erl b/apps/graphdb/src/graphdb_rules.erl index 6d4ea91..e1b5a63 100644 --- a/apps/graphdb/src/graphdb_rules.erl +++ b/apps/graphdb/src/graphdb_rules.erl @@ -15,11 +15,11 @@ %% Stub implementation. %%--------------------------------------------------------------------- %% Rev A Date: June 2026 Author: David W. Thomas (david@davidwt.com) -%% F4 Phase A: rule meta-ontology seeding (Rule / CompositionRule / +%% Rule meta-ontology seeding (Rule / CompositionRule / %% ConnectionRule), Rule Literals sub-group, applies_to/applied_by %% relationship-attribute pair, and seeded_nrefs/0. Idempotent init/1 %% mirrors graphdb_language. Rule create/retrieve/validation land in -%% later F4 Phase A tasks. +%% later tasks. %%--------------------------------------------------------------------- -module(graphdb_rules). -behaviour(gen_server). @@ -200,7 +200,7 @@ create_composition_rule(Scope, Name, ParentClass, ChildClass, Mode, Mult, %% reciprocal_nref, target_class_nref, optional template_nref) lives on the %% node; rule deployment (Template, mode, multiplicity) lives on the applies_to %% connection arc from the owning (source) class to the rule instance. Recip is -%% the reverse arc label (B4-D3): the arc as seen from the target back. Scope +%% the reverse arc label: the arc as seen from the target back. Scope %% environment writes to the shared ontology; {project, _} is not supported. %%----------------------------------------------------------------------------- create_connection_rule(Scope, Name, SourceClass, Char, Recip, TargetClass, @@ -231,7 +231,7 @@ get_rule(Scope, RuleNref) -> %% applies_to connection arcs out of ClassNref. {project, _} -> {ok, []}. %% DIRECT attachments only: rules attached to ClassNref's taxonomy %% ancestors are NOT included. Ancestor-walking (effective_rules_for_class) -%% is a Phase B addition. +%% is a later-phase addition. %%----------------------------------------------------------------------------- rules_for_class(Scope, ClassNref) -> gen_server:call(?MODULE, {rules_for_class, Scope, ClassNref}). @@ -264,7 +264,7 @@ connection_rules_for_class(Scope, ClassNref) -> %% {project, _} -> {ok, []}. %% %% Does NOT resolve override/shadow/conflict -- every level's rules are -%% present. Resolution is the firing engine's job (Phase B2/B5). +%% present. Resolution is the firing engine's job. %%----------------------------------------------------------------------------- effective_rules_for_class(Scope, ClassNref) -> gen_server:call(?MODULE, {effective_rules_for_class, Scope, ClassNref}). @@ -276,11 +276,12 @@ effective_rules_for_class(Scope, ClassNref) -> %% reciprocal := integer(), %% target_class := integer()}}]} %% -%% The effective rules of ClassNref (self + taxonomy ancestors, nearest-first; -%% B1) filtered to the ConnectionRule meta-class, each paired with its applies_to -%% deployment and a ConnSpec decoded from the rule node's content AVPs. The B4 -%% firing engine consumes this during create_instance. Additive -- a rule reached -%% from two ancestors appears twice (precedence is B5). {project, _} -> {ok, []}. +%% The effective rules of ClassNref (self + taxonomy ancestors, nearest-first) +%% filtered to the ConnectionRule meta-class, each paired with its applies_to +%% deployment and a ConnSpec decoded from the rule node's content AVPs. The +%% connection-firing engine consumes this during create_instance. Additive -- a +%% rule reached from two ancestors appears twice (precedence is a later phase). +%% {project, _} -> {ok, []}. %%----------------------------------------------------------------------------- effective_connection_rules(Scope, ClassNref) -> gen_server:call(?MODULE, {effective_connection_rules, Scope, ClassNref}). @@ -314,7 +315,7 @@ list_rules(Scope) -> %% %% Error reasons: %% {class_not_instantiable, ChildClassNref} -- -%% a mandatory rule's child_class is abstract (L9) +%% a mandatory rule's child_class is abstract %% %% Scope {project, _} returns a leaf plan immediately (no rule lookup). %%----------------------------------------------------------------------------- @@ -522,7 +523,7 @@ code_change(_OldVsn, State, _Extra) -> %%--------------------------------------------------------------------- -%% Seeding helpers (idempotent -- see F4 Phase A plan Architecture Notes) +%% Seeding helpers (idempotent -- see the rules-engine design) %%--------------------------------------------------------------------- %% ensure_seed(Name, ParentNref) -> Nref @@ -611,7 +612,7 @@ class_has_name(#node{attribute_value_pairs = AVPs}, Name) -> end, AVPs). %% instantiable_marker_nref/0 -> InstAttrNref -%% Reads the seeded `instantiable' marker nref from graphdb_attr (L9). +%% Reads the seeded `instantiable' marker nref from graphdb_attr. instantiable_marker_nref() -> {ok, #{instantiable := InstAttr}} = graphdb_attr:seeded_nrefs(), InstAttr. @@ -678,7 +679,7 @@ validate_multiplicity(_) -> %% validate_owning_class(Nref) -> ok | {error, atom()} %% The owning class must exist, be a class, and have a default template -- %% the applies_to arc stamps the default template as deployment AVP index 0. -%% An abstract class (L9 instantiable=false) or a class whose default +%% An abstract class (instantiable=false) or a class whose default %% template was deleted ("forced disambiguation") has none; reject cleanly %% rather than let do_create_rule badmatch. validate_owning_class(Nref) -> @@ -753,7 +754,7 @@ validate_template(Nref) -> %% arc pair (chars 29/30), and the applies_to/applied_by connection arc %% pair. Rule content lives on the node; rule deployment (Template, mode, %% multiplicity) lives on the forward applies_to arc only. -%% (Validation is added in a later F4 Phase A task; for now it writes +%% (Validation is added in a later task; for now it writes %% directly.) do_create_rule(MetaClassNref, Name, OwningClass, ContentAVPs, Mode, Mult, State) -> @@ -814,7 +815,7 @@ optional_template_avp(TemplateNref, State) -> [#{attribute => State#state.template_nref_attr, value => TemplateNref}]. %% optional_name_pattern_avp(Opts, State) -> [AVP] | [] -%% The optional name_pattern content AVP on the rule node (B2-D7). +%% The optional name_pattern content AVP on the rule node. optional_name_pattern_avp(Opts, State) -> case maps:get(name_pattern, Opts, undefined) of undefined -> []; @@ -850,7 +851,7 @@ attached_rules(ClassNref, State) -> %% Self-first, nearest-first taxonomy gather: the class itself followed by its %% ancestors (graphdb_class:ancestors/1 order). Each level carries the rules %% attached directly to it, paired with that attachment's deployment. Levels -%% with no attached rules are dropped (B1-D7). Resolves nothing (B1-D1). +%% with no attached rules are dropped. Resolves nothing. effective_rules(ClassNref, State) -> Chain = [ClassNref | ancestor_nrefs(ClassNref)], [{Level, Pairs} @@ -861,8 +862,8 @@ effective_rules(ClassNref, State) -> %% ancestor_nrefs(ClassNref) -> [integer()] %% The taxonomy ancestors of ClassNref, nearest-first, via the canonical %% graphdb_class:ancestors/1 walk. A bad starting class (unknown nref or a -%% non-class node) makes ancestors/1 return {error, _}; B1 maps that to an -%% empty ancestor set (B1-D6). The direct-attachment read on a bad nref is +%% non-class node) makes ancestors/1 return {error, _}; this maps that to an +%% empty ancestor set. The direct-attachment read on a bad nref is %% likewise empty, so the overall effective result is {ok, []}. ancestor_nrefs(ClassNref) -> case graphdb_class:ancestors(ClassNref) of @@ -882,9 +883,9 @@ attached_rules_with_deployment(ClassNref, State) -> %% decode_deployment(AVPs, State) -> map() %% Decodes an applies_to arc's deployment AVPs into the symbolic Deployment map %% #{mode, multiplicity, template}. A key whose AVP is absent is omitted -%% (B1-D2). The `template' key reads the arc Template scope marker +%%. The `template' key reads the arc Template scope marker %% (?ARC_TEMPLATE, attr 31) -- NOT the template_nref content literal on the -%% rule node. 'multiplicity' is a {Min, Max} range (B-prep); the fold copies +%% rule node. 'multiplicity' is a {Min, Max} range; the fold copies %% it verbatim. decode_deployment(AVPs, State) -> Pairs = [{mode, State#state.mode_attr}, @@ -919,7 +920,7 @@ meta_nref(connection_rule, State) -> State#state.connection_rule_nref. %%--------------------------------------------------------------------- -%% Plan path (pure read -- B2) +%% Plan path (pure read) %%--------------------------------------------------------------------- %% plan_composition_firing/2 runs inside the gen_server process. It calls %% effective_rules/2 (the internal state-passing helper) directly -- calling @@ -929,7 +930,7 @@ meta_nref(connection_rule, State) -> State#state.connection_rule_nref. %% leaf_plan(ClassNref, Rule, Deploy, Name) -> PlanNode %% Deploy is the deployment map of the rule that mandated this node %% (`undefined` for the root). Carried so the report's `deployment` field -%% is the real #{mode, multiplicity, template} (B2-D6). +%% is the real #{mode, multiplicity, template}. leaf_plan(ClassNref, Rule, Deploy, Name) -> #{class => ClassNref, name => Name, rule => Rule, deploy => Deploy, mandatory_children => [], auto_rules => [], propose_rules => []}. @@ -937,7 +938,7 @@ leaf_plan(ClassNref, Rule, Deploy, Name) -> %% plan_node(ClassNref, Rule, Deploy, Name, OnPath, State) %% -> {ok, PlanNode} | {error, Reason, #{plan_so_far, culprit}} %% Recursively expands the mandatory cascade for ClassNref. OnPath is the -%% class path root->here (B2-D5 cycle guard). Rule/Deploy describe the +%% class path root->here (cycle guard). Rule/Deploy describe the %% composition rule that mandated this node (`root`/`undefined` for the %% requested instance). plan_node(ClassNref, Rule, Deploy, Name, OnPath, State) -> @@ -982,7 +983,7 @@ connection_spec(RuleNode, State) -> content_avp_value(RuleNode, State#state.target_class_nref_attr)}. %% plan_rules(Pairs, OnPath1, State, Acc) -> {ok, PlanNode} | {error, R, Failure} -%% First-failure-aborts (B2-D6): a mandatory violation stops planning. +%% First-failure-aborts: a mandatory violation stops planning. plan_rules([], _OnPath1, _State, Acc) -> {ok, Acc}; plan_rules([{RuleNode, Deploy} | Rest], OnPath1, State, Acc) -> @@ -991,7 +992,7 @@ plan_rules([{RuleNode, Deploy} | Rest], OnPath1, State, Acc) -> Autos = maps:get(auto_rules, Acc) ++ [{RuleNode, Deploy}], plan_rules(Rest, OnPath1, State, Acc#{auto_rules => Autos}); propose -> - %% B3: accumulate (B2 dropped these). Mirrors the `auto` clause; + %% Accumulate (composition firing dropped these). Mirrors the `auto` clause; %% graphdb_instance:fire_propose/2 expands multiplicity post-commit %% and emits `proposed` outcomes. Unexpanded here, like auto_rules. Proposes = maps:get(propose_rules, Acc) ++ [{RuleNode, Deploy}], @@ -1012,7 +1013,7 @@ plan_mandatory(RuleNode, Deploy, OnPath1, State, Acc) -> State#state.child_class_nref_attr), case lists:member(ChildClass, OnPath1) of true -> - {ok, Acc}; %% B2-D5 zero-level cut: self-nest, no fire + {ok, Acc}; %% zero-level cut: self-nest, no fire false -> {Min, _Max} = maps:get(multiplicity, Deploy, {1, 1}), case graphdb_class:is_instantiable(ChildClass) of @@ -1047,7 +1048,7 @@ expand_children(RuleNode, Deploy, ChildClass, Mult, I, OnPath1, State, Acc) -> {error, R, Failure} -> %% Nested failure: rewrite plan_so_far to THIS level's Acc (parent %% with completed siblings; failing branch dropped), keep the leaf - %% culprit. (B2 design §3.1 trace.) + %% culprit. (composition-firing design §3.1 trace.) {error, R, Failure#{plan_so_far => Acc}} end. diff --git a/apps/graphdb/test/graphdb_attr_SUITE.erl b/apps/graphdb/test/graphdb_attr_SUITE.erl index c767e9e..39eb01a 100644 --- a/apps/graphdb/test/graphdb_attr_SUITE.erl +++ b/apps/graphdb/test/graphdb_attr_SUITE.erl @@ -87,7 +87,7 @@ get_attribute_rejects_non_attribute/1, list_attributes_includes_bootstrap_and_runtime/1, list_relationship_types_includes_buckets/1, - %% Attribute type (M8) + %% Attribute type seeded_nrefs_includes_attribute_type/1, create_name_stamps_attribute_type/1, create_literal_stamps_attribute_type/1, @@ -454,7 +454,7 @@ create_relationship_attribute_pair(_Config) -> Rev#node.attribute_value_pairs)). %%----------------------------------------------------------------------------- -%% M4: create_relationship_attribute commits both nodes plus all four +%% create_relationship_attribute commits both nodes plus all four %% taxonomy arc rows in a single transaction. After a successful %% call the row deltas must be exactly +2 on `nodes` and +4 on %% `relationships` -- no orphan halves, no double-counted arcs. @@ -724,7 +724,7 @@ list_relationship_types_includes_buckets(_Config) -> %%============================================================================= -%% Attribute Type Tests (M8) +%% Attribute Type Tests %%============================================================================= %%----------------------------------------------------------------------------- diff --git a/apps/graphdb/test/graphdb_class_SUITE.erl b/apps/graphdb/test/graphdb_class_SUITE.erl index b0c84a0..040e02a 100644 --- a/apps/graphdb/test/graphdb_class_SUITE.erl +++ b/apps/graphdb/test/graphdb_class_SUITE.erl @@ -98,7 +98,7 @@ subclasses_empty_for_leaf/1, ancestors_returns_chain/1, ancestors_empty_for_top_level/1, - %% Multi-inheritance (H3) + %% Multi-inheritance add_superclass_basic/1, add_superclass_writes_taxonomy_arcs/1, add_superclass_idempotent/1, @@ -729,7 +729,7 @@ ancestors_empty_for_top_level(_Config) -> %%============================================================================= -%% Multi-Inheritance Tests (H3) +%% Multi-Inheritance Tests %%============================================================================= %%----------------------------------------------------------------------------- diff --git a/apps/graphdb/test/graphdb_instance_SUITE.erl b/apps/graphdb/test/graphdb_instance_SUITE.erl index d401614..ec0fe7e 100644 --- a/apps/graphdb/test/graphdb_instance_SUITE.erl +++ b/apps/graphdb/test/graphdb_instance_SUITE.erl @@ -110,7 +110,7 @@ resolve_value_source_class/1, resolve_value_source_ancestor/1, resolve_value_source_connected/1, - %% Multi-membership (H4) + %% Multi-membership add_class_membership_basic/1, add_class_membership_writes_arcs/1, add_class_membership_idempotent/1, @@ -120,13 +120,13 @@ add_class_membership_rejects_non_class_target/1, add_class_membership_refuses_abstract_class/1, class_memberships_initial/1, - %% Multi-membership resolver (H5) + %% Multi-membership resolver resolve_value_unique_across_two_classes/1, resolve_value_same_value_two_classes/1, resolve_value_ambiguous_two_classes/1, resolve_value_local_overrides_ambiguity/1, resolve_value_ambiguity_via_taxonomy/1, - %% Firing (B2) + %% Firing firing_no_rules_baseline/1, firing_single_mandatory/1, firing_mandatory_mult/1, @@ -145,19 +145,19 @@ firing_propose_owner_is_materialised_child/1, firing_propose_carries_max/1, firing_propose_min_zero_surfaces_none/1, - %% B-prep mint-Min (BP-D2/BP-D3) + %% mint-Min firing_mandatory_mints_min/1, firing_mandatory_min_zero_mints_none/1, firing_mandatory_min_unbounded_mints_min/1, firing_auto_mints_min/1, firing_auto_min_zero_unbounded/1, - %% B4 connection firing + %% connection firing firing_conn_report_only_mandatory/1, firing_conn_report_only_auto/1, firing_conn_report_only_propose/1, firing_conn_explicit_defer/1, firing_conn_summarize/1, - %% B4 mandatory commit path + %% mandatory commit path firing_conn_mandatory_connected/1, firing_conn_mandatory_shortfall_fails/1, firing_conn_mandatory_invalid_target_fails/1, @@ -165,10 +165,10 @@ firing_conn_rollback_discriminable_composition/1, firing_conn_rollback_discriminable_connection/1, firing_conn_descendant_in_root_txn/1, - %% B4 auto connection post-commit + %% auto connection post-commit firing_conn_auto_connected/1, firing_conn_auto_invalid_survives/1, - %% B4 target validation (B4-D6) + %% target validation firing_conn_subclass_target_accepted/1, firing_conn_missing_target_fails/1, firing_conn_non_instance_target_fails/1, @@ -715,7 +715,7 @@ add_relationship_no_default_after_delete(_Config) -> graphdb_instance:add_relationship(A, Char, B, Recip)). %%----------------------------------------------------------------------------- -%% M3: missing source nref is rejected. +%% missing source nref is rejected. %%----------------------------------------------------------------------------- add_relationship_rejects_missing_source(_Config) -> {ok, ClassNref} = graphdb_class:create_class("Thing", 3), @@ -726,7 +726,7 @@ add_relationship_rejects_missing_source(_Config) -> graphdb_instance:add_relationship(99999, Char, B, Recip)). %%----------------------------------------------------------------------------- -%% M3: missing target nref is rejected. +%% missing target nref is rejected. %%----------------------------------------------------------------------------- add_relationship_rejects_missing_target(_Config) -> {ok, ClassNref} = graphdb_class:create_class("Thing", 3), @@ -737,7 +737,7 @@ add_relationship_rejects_missing_target(_Config) -> graphdb_instance:add_relationship(A, Char, 99999, Recip)). %%----------------------------------------------------------------------------- -%% M3: characterization that is not kind=attribute is rejected. Uses +%% characterization that is not kind=attribute is rejected. Uses %% the bootstrap Projects category (nref 5) as a non-attribute node. %%----------------------------------------------------------------------------- add_relationship_rejects_non_attribute_char(_Config) -> @@ -750,7 +750,7 @@ add_relationship_rejects_non_attribute_char(_Config) -> graphdb_instance:add_relationship(A, 5, B, Recip)). %%----------------------------------------------------------------------------- -%% M3: reciprocal that is not kind=attribute is rejected. +%% reciprocal that is not kind=attribute is rejected. %%----------------------------------------------------------------------------- add_relationship_rejects_non_attribute_reciprocal(_Config) -> {ok, ClassNref} = graphdb_class:create_class("Thing", 3), @@ -762,7 +762,7 @@ add_relationship_rejects_non_attribute_reciprocal(_Config) -> graphdb_instance:add_relationship(A, Char, B, 5)). %%----------------------------------------------------------------------------- -%% M3: target whose kind disagrees with the characterization's +%% target whose kind disagrees with the characterization's %% target_kind AVP is rejected. Char declares target_kind=class but the %% target is an instance. %%----------------------------------------------------------------------------- @@ -778,7 +778,7 @@ add_relationship_rejects_target_kind_mismatch(_Config) -> %%----------------------------------------------------------------------------- -%% M5: /6 stamps user AVPs on both connection rows alongside the +%% /6 stamps user AVPs on both connection rows alongside the %% Template AVP. Same AVPs are seen in fwd and rev directions when %% they're identical lists. %%----------------------------------------------------------------------------- @@ -806,7 +806,7 @@ add_relationship_stamps_user_avps(_Config) -> ?assert(lists:member(UserAVP, Fwd#relationship.avps)). %%----------------------------------------------------------------------------- -%% M5: forward and reverse AVPs are per-direction independent. An AVP +%% forward and reverse AVPs are per-direction independent. An AVP %% supplied only on the forward side must not leak into the reverse arc. %%----------------------------------------------------------------------------- add_relationship_avps_are_per_direction(_Config) -> @@ -842,7 +842,7 @@ add_relationship_avps_are_per_direction(_Config) -> ?assertNot(lists:member(FwdOnly, Rev#relationship.avps)). %%----------------------------------------------------------------------------- -%% M5: /4 and /5 default to {[],[]}, so connection rows carry only the +%% /4 and /5 default to {[],[]}, so connection rows carry only the %% Template AVP. %%----------------------------------------------------------------------------- add_relationship_default_avps_empty(_Config) -> @@ -1062,7 +1062,7 @@ resolve_value_priority_ancestor_over_connected(_Config) -> graphdb_instance:resolve_value(Child, TestAttr)). %%----------------------------------------------------------------------------- -%% H1: resolve_from_class must walk the class taxonomy. Animal IS-A +%% resolve_from_class must walk the class taxonomy. Animal IS-A %% Mammal IS-A Dog: an attribute bound on Animal must be visible to a %% Dog instance even when neither Mammal nor Dog defines it. %%----------------------------------------------------------------------------- @@ -1078,7 +1078,7 @@ resolve_value_walks_class_taxonomy(_Config) -> graphdb_instance:resolve_value(Rex, TestAttr)). %%----------------------------------------------------------------------------- -%% H1: when both the local class and a taxonomy ancestor bind the same +%% when both the local class and a taxonomy ancestor bind the same %% attribute, the nearest class wins (taxonomy walk is nearest-first). %%----------------------------------------------------------------------------- resolve_value_local_class_overrides_taxonomy_ancestor(_Config) -> @@ -1092,7 +1092,7 @@ resolve_value_local_class_overrides_taxonomy_ancestor(_Config) -> graphdb_instance:resolve_value(Rex, TestAttr)). %%----------------------------------------------------------------------------- -%% H2: Priority 4 ("directly connected nodes") must consider only +%% Priority 4 ("directly connected nodes") must consider only %% connection-kind arcs. A value bound on the compositional parent's %% category (reached only via the parent_arc) must not surface via P4. %%----------------------------------------------------------------------------- @@ -1173,7 +1173,7 @@ resolve_value_source_connected(_Config) -> %%============================================================================= -%% Multi-Membership Tests (H4) +%% Multi-Membership Tests %%============================================================================= %%----------------------------------------------------------------------------- @@ -1272,7 +1272,7 @@ add_class_membership_rejects_non_class_target(_Config) -> %%----------------------------------------------------------------------------- %% A non-instantiable (abstract) class target is rejected — an instance %% cannot become a member of an abstract class (that would make it an -%% instance of one). Same guard as create_instance (L9). +%% instance of one). Same guard as create_instance. %%----------------------------------------------------------------------------- add_class_membership_refuses_abstract_class(_Config) -> {ok, #{instantiable := Inst}} = graphdb_attr:seeded_nrefs(), @@ -1295,7 +1295,7 @@ class_memberships_initial(_Config) -> %%============================================================================= -%% Multi-Membership Resolver Tests (H5) +%% Multi-Membership Resolver Tests %%============================================================================= %%----------------------------------------------------------------------------- @@ -1386,7 +1386,7 @@ resolve_value_ambiguity_via_taxonomy(_Config) -> %%============================================================================= -%% Firing Tests (B2) +%% Firing Tests %%============================================================================= %%----------------------------------------------------------------------------- @@ -1426,7 +1426,7 @@ firing_mandatory_mult(Config) -> graphdb_instance:create_instance("car", Owner, 5), ?assertEqual(3, length(Outs)), ?assertEqual([1, 2, 3], [maps:get(index, O) || O <- Outs]), - %% B2-D6: report carries the rule's real deployment map + %% report carries the rule's real deployment map ?assertEqual({3, 3}, maps:get(multiplicity, Dep)), ?assertEqual(mandatory, maps:get(mode, Dep)). @@ -1510,7 +1510,7 @@ firing_auto_cascade_merges(Config) -> graphdb_instance:summarize(Report)). %%----------------------------------------------------------------------------- -%% B3: a propose rule surfaces a `proposed` outcome carrying owner (the +%% a propose rule surfaces a `proposed` outcome carrying owner (the %% materialised parent), proposed_class, index and name — and creates NOTHING. %%----------------------------------------------------------------------------- firing_propose_outcome_in_report(Config) -> @@ -1529,7 +1529,7 @@ firing_propose_outcome_in_report(Config) -> ?assertNot(maps:is_key(child, Outcome)). %% no created-instance key %%----------------------------------------------------------------------------- -%% B3: a propose rule materialises nothing — node table size is unchanged +%% a propose rule materialises nothing — node table size is unchanged %% beyond the single root instance. %%----------------------------------------------------------------------------- firing_propose_not_materialised(Config) -> @@ -1542,7 +1542,7 @@ firing_propose_not_materialised(Config) -> ?assertEqual(Before + 1, After). %% only the root, no proposed children %%----------------------------------------------------------------------------- -%% B3: multiplicity=3 propose yields three proposed outcomes, indices 1..3, +%% multiplicity=3 propose yields three proposed outcomes, indices 1..3, %% names per name_pattern. %%----------------------------------------------------------------------------- firing_propose_multiplicity_bounded(Config) -> @@ -1559,8 +1559,8 @@ firing_propose_multiplicity_bounded(Config) -> ?assert(lists:all(fun(O) -> maps:get(status, O) =:= proposed end, Outs)). %%----------------------------------------------------------------------------- -%% B-prep: {1, unbounded} propose yields one proposed outcome (index 1) carrying -%% max => unbounded. The old index=unbounded sentinel is retired (BP-D3). +%% {1, unbounded} propose yields one proposed outcome (index 1) carrying +%% max => unbounded. The old index=unbounded sentinel is retired. %%----------------------------------------------------------------------------- firing_propose_multiplicity_unbounded(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1574,7 +1574,7 @@ firing_propose_multiplicity_unbounded(Config) -> ?assertEqual(unbounded, Max). %%----------------------------------------------------------------------------- -%% B3 OI-B3-2: a propose rule whose child class is already on the +%% a propose rule whose child class is already on the %% root->here path is cut — no proposed outcome. Owner's class proposes %% Owner (self), so nothing is surfaced. %%----------------------------------------------------------------------------- @@ -1586,7 +1586,7 @@ firing_propose_on_path_cut(Config) -> ?assertEqual([], Report). %%----------------------------------------------------------------------------- -%% B3: summarize/1 counts proposed outcomes (and the map gains the key). +%% summarize/1 counts proposed outcomes (and the map gains the key). %%----------------------------------------------------------------------------- firing_propose_summarize(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1598,7 +1598,7 @@ firing_propose_summarize(Config) -> graphdb_instance:summarize(Report)). %%----------------------------------------------------------------------------- -%% B3: all three modes on one create — mandatory + auto materialise, propose +%% all three modes on one create — mandatory + auto materialise, propose %% is surfaced but not materialised. %%----------------------------------------------------------------------------- firing_propose_with_mandatory_and_auto(Config) -> @@ -1621,7 +1621,7 @@ firing_propose_with_mandatory_and_auto(Config) -> graphdb_instance:summarize(Report)). %%----------------------------------------------------------------------------- -%% B3: a propose rule on a MANDATORY child's class surfaces a proposed outcome +%% a propose rule on a MANDATORY child's class surfaces a proposed outcome %% whose owner is the materialised child (NOT the root) — proves owner rides %% proposals at depth, not just at the requested-instance level. %%----------------------------------------------------------------------------- @@ -1647,7 +1647,7 @@ firing_propose_owner_is_materialised_child(Config) -> %%----------------------------------------------------------------------------- -%% B-prep BP-D2(b): propose {3, 5} surfaces 3 outcomes, each carrying max => 5. +%% propose {3, 5} surfaces 3 outcomes, each carrying max => 5. %%----------------------------------------------------------------------------- firing_propose_carries_max(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1663,7 +1663,7 @@ firing_propose_carries_max(Config) -> ?assertEqual([], [O || O <- Outs, maps:get(index, O) =:= unbounded]). %%----------------------------------------------------------------------------- -%% B-prep: {0, K} propose surfaces nothing by default (Min = 0); the ceiling K +%% {0, K} propose surfaces nothing by default (Min = 0); the ceiling K %% is for the future interactive-creation session (BP-OI-1). %%----------------------------------------------------------------------------- firing_propose_min_zero_surfaces_none(Config) -> @@ -1674,7 +1674,7 @@ firing_propose_min_zero_surfaces_none(Config) -> ?assertEqual(0, maps:get(proposed, graphdb_instance:summarize(Report))). %%----------------------------------------------------------------------------- -%% B-prep BP-D2: mandatory composition mints Min children. +%% mandatory composition mints Min children. %%----------------------------------------------------------------------------- firing_mandatory_mints_min(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1687,7 +1687,7 @@ firing_mandatory_mints_min(Config) -> ?assertEqual([1, 2], [maps:get(index, O) || O <- Fired]). %%----------------------------------------------------------------------------- -%% B-prep: {0, K} mandatory mints nothing (vacuous) and does not fail. +%% {0, K} mandatory mints nothing (vacuous) and does not fail. %%----------------------------------------------------------------------------- firing_mandatory_min_zero_mints_none(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1699,7 +1699,7 @@ firing_mandatory_min_zero_mints_none(Config) -> graphdb_instance:summarize(Report)). %%----------------------------------------------------------------------------- -%% B-prep BP-D3: {1, unbounded} mandatory mints Min (1) — no +%% {1, unbounded} mandatory mints Min (1) — no %% unbounded_multiplicity_not_fireable. %%----------------------------------------------------------------------------- firing_mandatory_min_unbounded_mints_min(Config) -> @@ -1715,7 +1715,7 @@ firing_mandatory_min_unbounded_mints_min(Config) -> end, Outs)). %%----------------------------------------------------------------------------- -%% B-prep BP-D2: auto composition mints Min children post-commit. +%% auto composition mints Min children post-commit. %%----------------------------------------------------------------------------- firing_auto_mints_min(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1727,7 +1727,7 @@ firing_auto_mints_min(Config) -> ?assertEqual(2, length(Fired)). %%----------------------------------------------------------------------------- -%% B-prep BP-D3: {0, unbounded} auto mints nothing and does not fail. +%% {0, unbounded} auto mints nothing and does not fail. %%----------------------------------------------------------------------------- firing_auto_min_zero_unbounded(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1810,7 +1810,7 @@ do_delete_dir(Dir) -> %%============================================================================= -%% B4 Connection Firing Tests +%% Connection Firing Tests %%============================================================================= %% These cases build their own classes (NOT via setup_firing_fixtures) and %% exercise the RESOLVE defer-path: report-only (/3) and explicit defer-all (/4) @@ -1818,7 +1818,7 @@ do_delete_dir(Dir) -> %% proposed outcomes; nothing is connected. %% /3 report-only: a mandatory connection rule surfaces as `required`, nothing -%% connected, create succeeds (the /3 mandatory escape, B4-D4). +%% connected, create succeeds (the /3 mandatory escape). firing_conn_report_only_mandatory(_Config) -> {Src, Tgt, Char, Recip} = b4_conn_classes("Car", "Mfr", "made_by", "makes"), {ok, _} = graphdb_rules:create_connection_rule( @@ -1871,7 +1871,7 @@ firing_conn_summarize(_Config) -> %%============================================================================= -%% B4 Mandatory Commit Path Tests +%% Mandatory Commit Path Tests %%============================================================================= %% mandatory + committing resolver: arc pair written in the root txn; outcome @@ -2015,7 +2015,7 @@ firing_conn_descendant_in_root_txn(_Config) -> %%============================================================================= -%% B4 Auto Connection Post-Commit Tests +%% Auto Connection Post-Commit Tests %%============================================================================= %% auto + committing resolver: target connected post-commit; root survives. @@ -2044,7 +2044,7 @@ firing_conn_auto_invalid_survives(_Config) -> %%============================================================================= -%% B4 Target Validation Tests (B4-D6) +%% Target Validation Tests %%============================================================================= %% a target that is an instance of a SUBCLASS of target_class is accepted. @@ -2098,7 +2098,7 @@ firing_conn_resolver_avps_stamped(_Config) -> %%============================================================================= -%% B4 helpers +%% Connection-firing helpers %%============================================================================= %% the single outgoing connection arc (#relationship{}) from Source with char. diff --git a/apps/graphdb/test/graphdb_instance_tests.erl b/apps/graphdb/test/graphdb_instance_tests.erl index 84caad8..42b26e4 100644 --- a/apps/graphdb/test/graphdb_instance_tests.erl +++ b/apps/graphdb/test/graphdb_instance_tests.erl @@ -61,7 +61,7 @@ find_avp_value_undefined_does_not_shadow_bound_in_suffix_test() -> %%============================================================================= -%% Firing report helpers (B2-D6) tests +%% Firing report helpers tests %%============================================================================= mk_rule(N) -> {node, N, instance, [], [c], [#{attribute => 1, value => "r"}]}. diff --git a/apps/graphdb/test/graphdb_mgr_SUITE.erl b/apps/graphdb/test/graphdb_mgr_SUITE.erl index 57ff9f3..1740fcc 100644 --- a/apps/graphdb/test/graphdb_mgr_SUITE.erl +++ b/apps/graphdb/test/graphdb_mgr_SUITE.erl @@ -76,7 +76,7 @@ category_guard_allows_noncategory_delete/1, category_guard_allows_noncategory_update/1, category_guard_delete_nonexistent/1, - %% Write delegation (L4) + %% Write delegation create_name_attribute_delegates/1, create_literal_attribute_delegates/1, create_relationship_attribute_delegates/1, @@ -476,7 +476,7 @@ category_guard_delete_nonexistent(_Config) -> %%============================================================================= -%% Write Delegation Tests (L4) +%% Write Delegation Tests %% %% Each test verifies that graphdb_mgr correctly routes a write call to %% the appropriate worker and that the result is retrievable. Workers diff --git a/apps/graphdb/test/graphdb_query_SUITE.erl b/apps/graphdb/test/graphdb_query_SUITE.erl index a6d0dc1..ee965d3 100644 --- a/apps/graphdb/test/graphdb_query_SUITE.erl +++ b/apps/graphdb/test/graphdb_query_SUITE.erl @@ -8,7 +8,7 @@ %% Each testcase gets an isolated tmp dir + fresh Mnesia %% + fresh nref allocator + fully started graphdb %% supervision tree (mgr, attr, class, instance, language, -%% query). This is the F3 Task 2 smoke suite that asserts +%% query). This is the smoke suite that asserts %% the gen_server boots, the session API is sane, and %% every execute-path returns {error, not_implemented} %% until Tasks 3-9 land. @@ -45,7 +45,7 @@ new_session_has_snapshot/1, refresh_bumps_snapshot/1, unimplemented_query_returns_error/1, - %% Q1 — get_node + %% get_node q1_returns_bootstrap_node/1, q1_returns_attribute_node/1, q1_not_found_returns_error/1, @@ -60,29 +60,29 @@ q1b_nref_with_no_arcs/1, q1b_cache_uses_dir_kind_key/1, q1b_cache_hit_skips_mnesia/1, - %% Q2 — describe_attribute + %% describe_attribute q2_describes_name_attribute/1, q2_includes_parent_and_taxonomy/1, q2_includes_labels_default_english/1, q2_not_found_returns_error/1, q2_rejects_non_attribute_nref/1, - %% Q3 — describe_class + %% describe_class q3_describes_class_with_superclasses/1, q3_lists_subclasses/1, q3_includes_qcs_flat_list/1, q3_class_not_found/1, - %% Q4 — describe_instance + %% describe_instance q4_describes_instance_with_class/1, q4_resolves_inherited_attributes/1, q4_outgoing_and_incoming_connections/1, q4_compositional_ancestors/1, q4_instance_not_found/1, - %% Q5 — list_instances_of + %% list_instances_of q5_lists_direct_instances/1, q5_recursive_includes_subclass_instances/1, q5_non_recursive_excludes_subclasses/1, q5_class_with_no_instances/1, - %% Q6 — find_path + %% find_path q6_finds_path_via_taxonomy/1, q6_returns_no_path_when_disconnected/1, q6_respects_max_depth_returns_partial/1, @@ -294,13 +294,13 @@ refresh_bumps_snapshot(_Config) -> unimplemented_query_returns_error(_Config) -> %% A query shape the dispatcher will never recognise — exercises the - %% catch-all {error, not_implemented} path, durable across F3 tasks. + %% catch-all {error, not_implemented} path, durable across tasks. ?assertEqual({error, not_implemented}, graphdb_query:execute_query({unknown_query_shape, foo})). %%===================================================================== -%% Q1 — get_node tests +%% get_node tests %%===================================================================== q1_returns_bootstrap_node(_Config) -> @@ -458,7 +458,7 @@ q1b_cache_hit_skips_mnesia(_Config) -> %%===================================================================== -%% Q2 — describe_attribute tests +%% describe_attribute tests %%===================================================================== q2_describes_name_attribute(_Config) -> @@ -492,14 +492,14 @@ q2_not_found_returns_error(_Config) -> #q_describe{nref = 9999999, labels = default})). q2_rejects_non_attribute_nref(_Config) -> - %% NREF_ROOT is a category — Q2 path is for attributes only. + %% NREF_ROOT is a category — describe path is for attributes only. %% Categories take the category branch (no describe yet). {error, {unsupported_kind, category}} = graphdb_query:execute_query( #q_describe{nref = ?NREF_ROOT, labels = default}). %%--------------------------------------------------------------------- -%% Q3 — describe_class +%% describe_class %%--------------------------------------------------------------------- q3_describes_class_with_superclasses(_Config) -> %% Build: Classes <- Vehicle <- Car @@ -544,7 +544,7 @@ q3_class_not_found(_Config) -> #q_describe{nref = 9999999, labels = default})). %%--------------------------------------------------------------------- -%% Q4 — describe_instance +%% describe_instance %%--------------------------------------------------------------------- q4_describes_instance_with_class(_Config) -> {ok, Vehicle} = graphdb_class:create_class("Vehicle", ?NREF_CLASSES), @@ -613,7 +613,7 @@ q4_instance_not_found(_Config) -> #q_describe{nref = 9999999, labels = default})). %%--------------------------------------------------------------------- -%% Q5 — list_instances_of +%% list_instances_of %%--------------------------------------------------------------------- q5_lists_direct_instances(_Config) -> {ok, Veh} = graphdb_class:create_class("Vehicle", ?NREF_CLASSES), @@ -651,7 +651,7 @@ q5_class_with_no_instances(_Config) -> #q_instances_of{class = Veh, recursive = true})). %%--------------------------------------------------------------------- -%% Q6 — find_path +%% find_path %%--------------------------------------------------------------------- q6_finds_path_via_taxonomy(_Config) -> {ok, Veh} = graphdb_class:create_class("Vehicle", ?NREF_CLASSES), diff --git a/apps/graphdb/test/graphdb_rules_SUITE.erl b/apps/graphdb/test/graphdb_rules_SUITE.erl index d2f3daf..a320201 100644 --- a/apps/graphdb/test/graphdb_rules_SUITE.erl +++ b/apps/graphdb/test/graphdb_rules_SUITE.erl @@ -4,8 +4,8 @@ %%--------------------------------------------------------------------- %% Author: (completion of Dallas Noyes's design) %% Created: June 2026 -%% Description: Common Test integration suite for graphdb_rules (F4 -%% Phase A). Each test case gets its own isolated temp +%% Description: Common Test integration suite for graphdb_rules. +%% Each test case gets its own isolated temp %% directory with a fresh Mnesia database and nref %% allocator. Workers are started manually in dependency %% order; graphdb_rules is started last so its init/1 @@ -22,7 +22,7 @@ %% Record definitions (match graphdb internal records -- no shared %% header; copied verbatim from graphdb_instance_SUITE.erl). The %% #relationship{} record is exercised by the composition group -%% (read_arc/3) and later F4 Phase A tasks. +%% (read_arc/3) and later tasks. %%--------------------------------------------------------------------- -record(node, { nref, @@ -57,7 +57,7 @@ ]). %%--------------------------------------------------------------------- -%% Effective connection-rule test case exports (B4 Task 3) +%% Effective connection-rule test case exports %%--------------------------------------------------------------------- -export([ effective_connection_rules_returns_specs/1, @@ -66,7 +66,7 @@ ]). %%--------------------------------------------------------------------- -%% Plan firing test case exports (B2 Task 3) +%% Plan firing test case exports %%--------------------------------------------------------------------- -export([ plan_single_mandatory/1, @@ -137,9 +137,9 @@ mixed_rules_on_one_class/1, rule_isolation_across_class_taxonomy/1, duplicate_child_class_with_different_modes/1, - %% name_pattern (B2 Task 2) + %% name_pattern composition_rule_carries_name_pattern/1, - %% effective (B1 taxonomy walk) + %% effective self_only_no_ancestors/1, linear_chain_nearest_first/1, diamond_dag_dedup/1, @@ -401,7 +401,7 @@ seeds_rule_meta_ontology_idempotent(_Config) -> Comp = maps:get(composition_rule, S1), Conn = maps:get(connection_rule, S1), ?assert(is_integer(Rule)), - %% Rule is abstract (L9): is_instantiable/1 = false + %% Rule is abstract: is_instantiable/1 = false ?assertEqual(false, graphdb_class:is_instantiable(Rule)), %% Comp/Conn are instantiable subclasses of Rule ?assertEqual(true, graphdb_class:is_instantiable(Comp)), @@ -561,7 +561,7 @@ composition_rule_carries_name_pattern(_Config) -> ?assertEqual({ok, "Bolt {i}"}, find_avp(AVPs, NP)). %%----------------------------------------------------------------------------- -%% B3: a propose-mode rule lands in propose_rules (NOT auto_rules / +%% a propose-mode rule lands in propose_rules (NOT auto_rules / %% mandatory_children), unexpanded — exactly one {RuleNode, Deploy} entry %% regardless of multiplicity. %%----------------------------------------------------------------------------- @@ -576,7 +576,7 @@ plan_propose_accumulated(Config) -> ?assertEqual({3, 3}, maps:get(multiplicity, Dep)). %%----------------------------------------------------------------------------- -%% B3: one rule of each mode on the same owner populates all three +%% one rule of each mode on the same owner populates all three %% accumulators independently. %%----------------------------------------------------------------------------- plan_mixed_modes(Config) -> @@ -598,7 +598,7 @@ plan_mixed_modes(Config) -> [#{class := Bolt}] = Mand. %%----------------------------------------------------------------------------- -%% B3: a propose rule attached to a MANDATORY child's class appears in that +%% a propose rule attached to a MANDATORY child's class appears in that %% child's plan node (propose rides the mandatory-cascade recursion). %%----------------------------------------------------------------------------- plan_propose_at_mandatory_child(Config) -> @@ -803,7 +803,7 @@ invalid_multiplicity_rejected(_Config) -> environment, "x", Parent, Child, mandatory, "lots")), ?assertEqual(Before, table_size(nodes)). -%% B-prep BP-D4: the {Min, Max} validation catalogue. +%% the {Min, Max} validation catalogue. multiplicity_range_validation(_Config) -> Parent = make_class("Car"), Child = make_class("Engine"), @@ -920,7 +920,7 @@ list_rules_returns_all(_Config) -> %%============================================================================= %% Scope Tests %%============================================================================= -%% Phase A supports environment-scoped rules only. The {project, _} branches +%% The rules data model supports environment-scoped rules only. The {project, _} branches %% (added across Tasks 2-5) lock the contract: create is rejected, every %% retrieval returns empty / not_found. @@ -951,7 +951,7 @@ project_scope_returns_empty_on_retrieve(_Config) -> %% Complex Scenario Tests %%============================================================================= %% Integration tests over the Task 2-5 API. No new production code; these -%% exercise the create/retrieve paths in combination and document Phase A +%% exercise the create/retrieve paths in combination and document the data-model %% semantics (direct-attachment retrieval, no conflict resolution). %% Five distinct rules of both kinds attached to one owning class. Asserts @@ -993,9 +993,9 @@ mixed_rules_on_one_class(_Config) -> ?assertEqual(5, length(Applies)), ?assertEqual(ok, graphdb_mgr:verify_caches()). -%% Phase A retrieval is direct-attachment only: a rule on a superclass is NOT +%% Direct-attachment retrieval only: a rule on a superclass is NOT %% returned for a subclass. Documents that taxonomy-walking retrieval is -%% Phase B (effective_rules_for_class/2). +%% the taxonomy-walk read (effective_rules_for_class/2). rule_isolation_across_class_taxonomy(_Config) -> Vehicle = make_class("Vehicle"), {ok, Car} = graphdb_class:create_class("Car", Vehicle), @@ -1017,7 +1017,7 @@ rule_isolation_across_class_taxonomy(_Config) -> ?assertEqual(1, length(RS)). %% Two composition rules with the same child class but different modes are -%% both accepted (distinct nrefs). Documents that Phase A makes no +%% both accepted (distinct nrefs). Documents that the data model makes no %% conflict-resolution commitment. duplicate_child_class_with_different_modes(_Config) -> Cell = make_class("Cell"), @@ -1032,7 +1032,7 @@ duplicate_child_class_with_different_modes(_Config) -> %%============================================================================= -%% Effective Rules Tests (B1 -- taxonomy walk) +%% Effective Rules Tests %%============================================================================= %% effective_rules_for_class/2 gathers rules from the class AND its taxonomy %% ancestors, nearest-first, grouped by attaching class, each paired with that @@ -1090,7 +1090,7 @@ diamond_dag_dedup(_Config) -> shared_rule_node_across_ancestors(_Config) -> %% A and B are two superclasses of Bot. ONE rule node is attached to - %% BOTH (F4 D12 reuse). It must appear once per attaching ancestor, each + %% BOTH (rule reuse). It must appear once per attaching ancestor, each %% occurrence carrying that ancestor's own deployment. A = make_class("Insurable"), B = make_class("Taxable"), @@ -1119,8 +1119,8 @@ deployment_avps_surfaced(_Config) -> additive_parent_and_child(_Config) -> %% Parent mandates a wheel-group (mult 1); subclass adds more (mult 4) for - %% the SAME child class. B1 drops nothing -- both survive, each with its - %% own deployment. The firing engine (B2/B5) decides additive-vs-shadow. + %% the SAME child class. the gather drops nothing -- both survive, each with its + %% own deployment. The firing engine decides additive-vs-shadow. Vehicle = make_class("Vehicle"), {ok, Car} = graphdb_class:create_class("Car", Vehicle), Wheel = make_class("Wheel"), @@ -1168,7 +1168,7 @@ mixed_kinds_returned(_Config) -> Comp = maps:get(composition_rule, S), Conn = maps:get(connection_rule, S), Pairs = pairs_at(Car, Levels), - %% B1-D4 consumer pattern: inline kind filter over the gathered pairs. + %% consumer pattern: inline kind filter over the gathered pairs. CompNrefs = [N#node.nref || {N, _D} <- Pairs, lists:member(Comp, N#node.classes)], ConnNrefs = [N#node.nref || {N, _D} <- Pairs, @@ -1224,7 +1224,7 @@ effective_connection_rules_project_scope_empty(_Config) -> %%============================================================================= -%% Plan Firing Tests (B2 Task 3) +%% Plan Firing Tests %%============================================================================= %% plan_single_mandatory/1 — one mandatory rule, mult=2, two Bolt children. @@ -1271,7 +1271,7 @@ plan_auto_annotated_not_expanded(Config) -> graphdb_rules:plan_composition_firing(environment, Owner), ?assertEqual(auto, maps:get(mode, Dep)). -%% B-prep: {Min, unbounded} mandatory mints Min (here 1) — the old +%% {Min, unbounded} mandatory mints Min (here 1) — the old %% unbounded_multiplicity_not_fireable error is retired. plan_unbounded_mandatory_mints_min(Config) -> {Owner, Bolt} = ?config(ob, Config), @@ -1307,13 +1307,13 @@ plan_cascade(Config) -> %% plan_cycle_self_nest_zero_children/1 — a class with a rule pointing back to %% itself (Folder→Folder) must produce zero mandatory_children for the root node -%% (on-path cycle cut at plan_mandatory level, B2-D5), not loop. +%% (on-path cycle cut at plan_mandatory level), not loop. plan_cycle_self_nest_zero_children(Config) -> %% Folder mandates Folder Folder = ?config(folder, Config), {ok, _} = graphdb_rules:create_composition_rule( environment, "FF", Folder, Folder, mandatory, {1, 1}), - {ok, #{mandatory_children := []}} = %% zero-level cut, B2-D5 + {ok, #{mandatory_children := []}} = %% zero-level cut graphdb_rules:plan_composition_firing(environment, Folder). %% plan_cycle_a_b_a/1 — two-class cycle: A→B (mandatory/1), B→A (mandatory/1). @@ -1374,7 +1374,7 @@ make_class(Name) -> Nref. %% make_abstract_class(Name) -> Nref -%% Creates an abstract class (L9 instantiable=false marker) under +%% Creates an abstract class (instantiable=false marker) under %% ?NREF_CLASSES. An abstract class is born without a default template, %% so it must be rejected as a rule owning class. make_abstract_class(Name) -> @@ -1421,7 +1421,7 @@ rule_nrefs_at(Level, Levels) -> %% attach_existing_rule(OwnerClass, RuleNref, Mode, Mult) -> ok %% Writes a SECOND applies_to/applied_by connection arc pair from OwnerClass to -%% an already-existing rule node (F4 D12 reuse), stamped with OwnerClass's own +%% an already-existing rule node (rule reuse), stamped with OwnerClass's own %% deployment. Connection arcs are not part of the parents/classes caches, so %% this does not disturb verify_caches/0. Used by %% shared_rule_node_across_ancestors to make one rule node reachable from two diff --git a/arcs-authoritative.md b/arcs-authoritative.md index 60356ff..fddcfa7 100644 --- a/arcs-authoritative.md +++ b/arcs-authoritative.md @@ -7,9 +7,9 @@ SPDX-License-Identifier: GPL-2.0-or-later ## Status -**Accepted** — landed as H0 (commits `d5a7244` H0a, `0b5fc43` H0b, -`ce07cb2` H0c, `9e5d64a` H0d, plus the H0e doc fold). The invariant -is summarised in [`ARCHITECTURE.md`](ARCHITECTURE.md) §3; this +**Accepted** — landed across commits `d5a7244`, `0b5fc43`, +`ce07cb2`, `9e5d64a`, plus the documentation fold. The invariant +is summarised in [`Architecture.md`](docs/Architecture.md) §3; this document is retained as the formal decision record. ## Context @@ -22,9 +22,10 @@ parents, compositional parents, class memberships — in two places today: instantiation arcs). - Fields on the `node` record (`parent`). -M1 (resolved in PR #10) called out this inconsistency for instances. -The same shape reappeared in H3 (multi-parent classes) and H4 -(multi-class instances). A uniform answer is needed before H3 lands. +The original instance inconsistency was called out and resolved in +PR #10. The same shape reappeared with multi-parent classes and +multi-class instances. A uniform answer is needed before the +multi-parent-class work lands. ## Decision @@ -108,14 +109,14 @@ the relationship in plain English so the file remains human-followable top-to-bottom. The existing inline `%%` comments already demonstrate the pattern. -Pre-H0 example (today): +Pre-migration example: ```erlang {node, 6, attribute, 2, {18, "Names"}, []}. {relationship, 2, 24, [], 23, 6, [], composition}. %% Attributes -> Names ``` -Post-H0d example: +Post-migration example: ```erlang {node, 6, attribute, {18, "Names"}, []}. %% parent comes from the arc below @@ -127,18 +128,19 @@ writes the arcs, then runs `graphdb_mgr:rebuild_caches/0` followed by `graphdb_mgr:verify_caches/0` as a final assertion. Any mismatch between the rebuilt caches and the arcs is a fatal startup error. -## Migration / H0 scope +## Migration scope -H0 landed across substeps H0a–H0e in PR #10 (commit `4e56761`); the -final fold + RESOLVED markers shipped in commit `f2fead8`. See git -history for the substep commits. +This work landed across several substeps in PR #10 (commit `4e56761`); +the final fold shipped in commit `f2fead8`. See git history for the +substep commits. ## Consequences Pro: - - M1 closed; H3 lands as a small additive change atop established - cache machinery; H4 follows the same pattern. + - The original inconsistency is closed; multi-parent classes land as + a small additive change atop established cache machinery; + multi-class instances follow the same pattern. - Future memoization and parallel-fetch optimizations are internal changes — no API or schema move. - Single read path everywhere; no special-case branches for @@ -156,7 +158,7 @@ Con: ## Future work -`ARCHITECTURE.md` §3 now carries the cache invariant summary. This -document remains as the frozen decision record. `ARCHITECTURE.md` may +`Architecture.md` §3 now carries the cache invariant summary. This +document remains as the frozen decision record. `Architecture.md` may itself be split into multiple focused documents at a later date; that decision is deferred. diff --git a/ARCHITECTURE.md b/docs/Architecture.md similarity index 88% rename from ARCHITECTURE.md rename to docs/Architecture.md index f399306..974002a 100644 --- a/ARCHITECTURE.md +++ b/docs/Architecture.md @@ -7,36 +7,37 @@ SPDX-License-Identifier: GPL-2.0-or-later > High-level shape of the system. Updated as the code's architecture changes — not as > implementation progresses within an already-described component. The canonical -> spec is [`the-knowledge-network.md`](the-knowledge-network.md); the kernel -> implements that model. Outstanding work is grouped by severity in -> `TASKS.md`. +> spec is [`TheKnowledgeNetwork.md`](TheKnowledgeNetwork.md); the kernel +> implements that model. Outstanding work is described in +> [`../TASKS.md`](../TASKS.md). --- ## 1. Status -| Component | State | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Build | Compiles clean — zero warnings (Erlang/OTP, the Open Telecom Platform, version 28 / rebar3 3.27) | -| `nref` subsystem | Fully implemented; backed by DETS (Disk-based Erlang Term Storage); `set_floor/1` API | -| `dictionary_imp` | Implemented; not yet wired to `dictionary_server` / `term_server` | -| `graphdb_bootstrap` | Implemented — Mnesia schema, table creation, scaffold loader | -| `graphdb_mgr` | Implemented — bootstrap startup, read API, category guard, cache audit/repair. Write-side delegation pending. | -| `graphdb_attr` | Implemented — attribute library (name, literal, relationship attributes) | -| `graphdb_class` | Implemented — taxonomic hierarchy with multi-parent inheritance (BFS — breadth-first search — over a DAG, a directed acyclic graph; H3); abstract (non-instantiable) classes via the `instantiable` marker (L9) | -| `graphdb_instance` | Implemented — compositional hierarchy + four-level inheritance with multi-class membership (H4) and ambiguity-detecting class resolver (H5); refuses instantiation/membership of abstract classes (L9); fires composition rules on `create_instance/3` (F4 B2) and surfaces `proposed` outcomes for propose-mode rules (F4 B3) | -| `graphdb_rules` | Implemented — F4 Phases A+B1+B2+B3: rule meta-ontology, applies_to attachment, scope-aware create/retrieve, taxonomy-walking effective-rules read, composition firing engine, propose mode | -| `graphdb_language` | Implemented — M6 multilingual overlay layer (label resolution, dialect chains, per-language Mnesia overlay tables) | -| `graphdb_query` | Implemented — F3 query language (Q1-Q6) with snapshot-semantics sessions and continuation-based bounded BFS | -| Tests | 476 passing (371 Common Test + 105 EUnit) | +| Component | State | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Build | Compiles clean — zero warnings (Erlang/OTP, the Open Telecom Platform, version 28 / rebar3 3.27) | +| `nref` subsystem | Fully implemented; backed by DETS (Disk-based Erlang Term Storage); `set_floor/1` API | +| `dictionary_imp` | Implemented; not yet wired to `dictionary_server` / `term_server` | +| `graphdb_bootstrap` | Implemented — Mnesia schema, table creation, scaffold loader | +| `graphdb_mgr` | Implemented — bootstrap startup, read API, category guard, cache audit/repair. Write-side delegation pending. | +| `graphdb_attr` | Implemented — attribute library (name, literal, relationship attributes) | +| `graphdb_class` | Implemented — taxonomic hierarchy with multi-parent inheritance (BFS — breadth-first search — over a DAG, a directed acyclic graph); abstract (non-instantiable) classes via the `instantiable` marker | +| `graphdb_instance` | Implemented — compositional hierarchy + four-level inheritance with multi-class membership and ambiguity-detecting class resolver; refuses instantiation/membership of abstract classes; fires composition rules on `create_instance/3` and surfaces `proposed` outcomes for propose-mode rules; fires connection rules via a caller-supplied resolver on `create_instance/4` | +| `graphdb_rules` | Implemented — rule meta-ontology, applies_to attachment, scope-aware create/retrieve, taxonomy-walking effective-rules read, composition firing engine, propose mode, connection firing | +| `graphdb_language` | Implemented — multilingual overlay layer (label resolution, dialect chains, per-language Mnesia overlay tables) | +| `graphdb_query` | Implemented — query language with snapshot-semantics sessions and continuation-based bounded BFS | +| Tests | 476 passing (371 Common Test + 105 EUnit) | The kernel is functional under multi-inheritance, multi-class- membership, and per-class template semantics. Multilingual label -overlay (M6, §10) and the F3 query language (§11) are landed. -The `graphdb_rules` data model (F4 Phase A, §12) is landed; the -taxonomy-walk read (Phase B1), composition firing engine (Phase B2), and -propose mode (Phase B3 — `create_instance/3` surfaces `proposed` -outcomes) are also landed. The firing engine (Phases B4–F) remains. +overlay (§10) and the query language (§11) are landed. +The `graphdb_rules` data model (§12) is landed, along with the +taxonomy-walk effective-rules read, the composition firing engine, +propose mode (`create_instance/3` surfaces `proposed` outcomes), and +connection firing. The later firing-engine work — conflict precedence, +the instantiation engine, and reactive learning — remains. --- @@ -100,7 +101,7 @@ no runtime API can create, modify, or delete a `category` node. `parents` and `classes` are **caches** of the authoritative arcs in the `relationships` table. The decision record is -[`arcs-authoritative.md`](arcs-authoritative.md); the rules are: +[`arcs-authoritative.md`](../arcs-authoritative.md); the rules are: 1. Every taxonomic, compositional, and instantiation relationship is canonical in `relationships`. @@ -197,7 +198,7 @@ expand a single bidirectional intent into two directed records. `avps` carries metadata that is asymmetric between the two directions — provenance, confidence, weights, validity time frames, flags. Per -[`the-knowledge-network.md`](the-knowledge-network.md) §5, this metadata +[`TheKnowledgeNetwork.md`](TheKnowledgeNetwork.md) §5, this metadata is part of the connection's identity for ASSOCIATE-type arcs, but does not participate in graph traversal by default. @@ -232,9 +233,9 @@ graphdb (application — started after mnesia + nref) ├── graphdb_attr — attribute library ├── graphdb_class — taxonomic hierarchy ├── graphdb_instance — compositional hierarchy + inheritance - ├── graphdb_language — multilingual label overlay (M6) - ├── graphdb_query — F3 query language gen_server - └── graphdb_rules — rule meta-ontology + create/retrieve + composition firing + propose mode (F4 A+B1+B2+B3) + ├── graphdb_language — multilingual label overlay + ├── graphdb_query — query language gen_server + └── graphdb_rules — rule meta-ontology + create/retrieve + composition firing + propose mode + connection firing dictionary (application — started alongside graphdb) └── dictionary_sup @@ -250,7 +251,7 @@ seerstone (application — top-level; started last) seerstone-specific workers ``` -`graphdb` and `dictionary` are independent peer applications (E5). +`graphdb` and `dictionary` are independent peer applications. `database_sup` is intentionally empty — it serves as an attachment point for any future database-level coordination services without reintroducing the `included_applications` coupling. @@ -258,7 +259,7 @@ any future database-level coordination services without reintroducing the Worker boundaries: each `graphdb_*` worker owns the schema/contract it maintains. `graphdb_mgr` is the public entry point and routes to the workers — read path implemented; write-side routing is pending -(`TASKS.md` L4). +(see [`../TASKS.md`](../TASKS.md)). --- @@ -459,17 +460,17 @@ create, modify, or delete a `category` node. ## 9. Inheritance Resolution `graphdb_instance:resolve_value/2` implements the four-level priority -order from [`the-knowledge-network.md`](the-knowledge-network.md) §6: +order from [`TheKnowledgeNetwork.md`](TheKnowledgeNetwork.md) §6: 1. **Local AVPs** on the instance — highest. 2. **Class-bound values** — every class membership in `node.classes`; for each, walk the class itself plus its taxonomic ancestor DAG (`graphdb_class:ancestors/1`, BFS over multi-parent - classes, nearest first; H3). Per-membership hits are gathered as + classes, nearest first). Per-membership hits are gathered as `[{ClassNref, Value}]` and reduced: a single distinct value wins (`{ok, Value}`); two or more distinct values produce `{error, {ambiguous_class_value, AttrNref, Hits}}`; zero hits fall - through (H4 + H5). + through. 3. **Compositional ancestors** — unbroken upward walk via the `node.parents` cache. Composition is a tree (one whole has at most one parent), so the walk is single-chain. @@ -540,9 +541,9 @@ class node to the appropriate subcategory (e.g., English → nref 32). The subcategory nodes are not parents in the class hierarchy; they are category anchors in the organisational scaffold. -### Current implementation — multilingual label overlay (M6) +### Current implementation — multilingual label overlay -The full language-project mechanism is a future capability. The current M6 +The full language-project mechanism is a future capability. The current implementation provides a pragmatic foundation: per-language Mnesia overlay tables (`language_en`, `language_de`, …) that store per-attribute label overrides keyed by nref. A language **chain** — an ordered list of language @@ -553,8 +554,8 @@ This overlay mechanism is designed as a replaceable abstraction: when language projects are built out, the backing will shift from flat per-nref rows to traversal into project instance graphs, and the overlay tables will become caches of that traversal. The `resolve_label/3` API does not change -when the backing changes. See `TASKS.md` F2 for current implementation -scope. +when the backing changes. See [`../TASKS.md`](../TASKS.md) for the +remaining multilingual write-path work. --- @@ -585,23 +586,25 @@ Architectural shape: in BFS expansion, matching the semantics already encoded in `graphdb_class:ancestors/1`'s NREF_CLASSES filter. -See `docs/designs/f3-graphdb-query-design.md` for the durable architectural +See `designs/f3-graphdb-query-design.md` for the durable architectural contract. --- -## 12. Rules (`graphdb_rules`, F4 Phases A + B1 + B2 + B3) +## 12. Rules (`graphdb_rules`) -`graphdb_rules` implements the rules data model and composition firing -engine. Phase A is storage and retrieval; Phase B1 adds taxonomy-walking -reads; Phase B2 adds the composition firing engine; Phase B3 adds propose -mode. Phases B4–F remain, tracked in `TASKS.md`. +`graphdb_rules` implements the rules data model and the firing engine: +storage and retrieval, taxonomy-walking effective-rules reads, the +composition firing engine, propose mode, and connection firing. The +later firing-engine work — conflict precedence, the instantiation +engine, and reactive learning — remains, tracked in +[`../TASKS.md`](../TASKS.md). Architectural shape: - A rule is a `kind = instance` node. Its class membership is one of two seeded meta-classes, `CompositionRule` or `ConnectionRule`, both - subclasses of an abstract `Rule` root (non-instantiable via the L9 + subclasses of an abstract `Rule` root (non-instantiable via the `instantiable` marker). The meta-ontology, a `Rule Literals` literal sub-group, and the `applies_to`/`applied_by` relationship-attribute pair are seeded idempotently at `init/1`; `graphdb_rules` is the last @@ -616,24 +619,25 @@ Architectural shape: the `applies_to`/`applied_by` connection pair between owning class and rule. `rules_for_class/2` is **direct-attachment only** — it reads the owning class's outgoing `applies_to` arcs. `effective_rules_for_class/2` - (Phase B / B1) additionally walks the class's taxonomy ancestors: + additionally walks the class's taxonomy ancestors: a nearest-first, deployment-bearing gather of every rule attached to the class and its superclasses, grouped by attaching class. It resolves - nothing — additive-vs-shadow is the firing engine's job (B5). -- **Composition firing (B2).** `graphdb_instance:create_instance/3` calls + nothing — additive-vs-shadow is the firing engine's job (conflict + precedence, still outstanding). +- **Composition firing.** `graphdb_instance:create_instance/3` calls `graphdb_rules:plan_composition_firing/2` to build an abstract plan tree, then executes it: `mandatory` rules fire inside the same transaction as the parent; `auto` rules fire post-commit. Return shape is `{ok, Nref, Report}` on success or `{error, Reason, Report}` on firing failure; pre- plan validation errors return `{error, Reason}` (2-tuple). The report is rule-centric: `[#{rule, deployment, outcomes}]`. -- **Propose mode (B3).** `propose`-mode composition rules materialise +- **Propose mode.** `propose`-mode composition rules materialise nothing; they surface as `proposed` outcomes in the same create report (always-in-report — no session flag). A caller accepts a proposal by issuing an ordinary `create_instance/3` for the proposed class. - **Scope.** The API is scope-tagged (`environment` | `{project, _}`). - Phase A serves the `environment` scope; `{project, _}` creates are + It serves the `environment` scope; `{project, _}` creates are rejected and `{project, _}` reads return empty. -See `docs/designs/f4-graphdb-rules-design.md` for the durable architectural +See `designs/f4-graphdb-rules-design.md` for the durable architectural contract. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 0000000..8f47b59 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,16 @@ +# docs/ — File Reference Notes + +## Renamed/moved files (2026-06-12) + +Two files that design docs and plan docs reference by their old names were +moved as part of a documentation reorganisation: + +| Old name (project root) | New location | +|-----------------------------|---------------------------------| +| `ARCHITECTURE.md` | `docs/Architecture.md` | +| `the-knowledge-network.md` | `docs/TheKnowledgeNetwork.md` | + +Design docs (`docs/designs/`) and plan docs (`docs/superpowers/plans/`) were +intentionally left with their original filename references so they remain +accurate historical records. When reading those docs, resolve the old names +using the table above. diff --git a/the-knowledge-network.md b/docs/TheKnowledgeNetwork.md similarity index 100% rename from the-knowledge-network.md rename to docs/TheKnowledgeNetwork.md diff --git a/docs/archive/TASKS-DONE.md b/docs/archive/TASKS-DONE.md new file mode 100644 index 0000000..ad3ffea --- /dev/null +++ b/docs/archive/TASKS-DONE.md @@ -0,0 +1,1091 @@ + + +# SeerStoneGraphDb — Resolved Tasks + +Archive of completed work, in the order it was resolved — the M-series +kernel-correctness fixes, the feature phases (language bootstrap, +multilingual layer, query language, rules data model and firing engine), +and the engineering-hygiene items. Each entry keeps its original phase +label and decision log. Active remaining work is in +[`../../TASKS.md`](../../TASKS.md). + +--- + +## M1. PART-OF stored in two places with no consistency invariant — RESOLVED + +**Status:** Closed by H0 (PR #10, commit `4e56761`). The decision: +arcs are authoritative, `node.parents`/`node.classes` are caches with +a hard invariant enforced by `graphdb_mgr:verify_caches/0` (run in +every CT `end_per_testcase` and at bootstrap load completion). +Single-writer ownership rule documented in `arcs-authoritative.md` +and `Architecture.md` §3. + +--- + +## M2. `resolve_from_class` should consult `graphdb_class`, not Mnesia directly — RESOLVED + +**Status:** Closed by H1. `resolve_from_class` now drives the class +walk through `graphdb_class:get_class/1` and +`graphdb_class:ancestors/1` instead of reading the `nodes` table +directly; the membership arc lookup reuses `do_class_of/1` so +`?CLASS_MEMBERSHIP_ARC` is no longer hard-coded inside the resolver. + +--- + +## M3. `add_relationship/4` validates nothing — RESOLVED + +**Status:** Fixed. `graphdb_instance:add_relationship` now runs an +explicit `validate_arc_endpoints/5` pass before resolving classes, +templates, and writing arcs. All four endpoint reads happen in one +`mnesia:transaction/1`. Failure modes are returned as structured +errors: + + - `{error, {source_not_found, Nref}}` + - `{error, {target_not_found, Nref}}` + - `{error, {characterization_not_found, Nref}}` + - `{error, {reciprocal_not_found, Nref}}` + - `{error, {characterization_not_an_attribute, Nref, ActualKind}}` + - `{error, {reciprocal_not_an_attribute, Nref, ActualKind}}` + - `{error, {target_kind_mismatch, ExpectedKind, ActualKind}}` + +The seeded `target_kind` literal-attribute nref is fetched from +`graphdb_attr:seeded_nrefs()` once at `graphdb_instance:init/1` and +cached in a new gen_server state record. Arc-label nodes that don't +carry a `target_kind` AVP skip the kind check. + +Tests: 5 CT cases under the `relationships` group covering the new +reject paths. + +--- + +## M4. Reciprocal attribute pair must be created in one transaction — RESOLVED + +**Status:** Fixed. `graphdb_attr:create_relationship_attribute/3` now +delegates to a private `do_create_relationship_attribute_pair/3` helper +that allocates the 2 node nrefs and 4 compositional arc-id nrefs +outside the transaction (avoiding side-effects on retry) and writes +all 6 rows in a single `mnesia:transaction/1`. Mid-pair aborts can no +longer leave the database with an orphan half-pair. + +Tests: CT case `create_relationship_attribute_pair_atomic` asserts +the row deltas are exactly +2 nodes and +4 relationships after a +successful call, and that both new nodes have exactly one parent→child +arc into them under the Relationships subtree (nref 8). + +--- + +## M5. `add_relationship` cannot accept per-arc AVPs at creation — RESOLVED + +**Status:** Fixed. New API +`graphdb_instance:add_relationship/6 :: (Source, Char, Target, Reciprocal, +TemplateNref, {FwdAVPs, RevAVPs}) -> ok | {error, _}` accepts +per-direction user AVPs and stamps them on the two connection rows +alongside the auto-applied Template AVP. Per-direction 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. + +Tests: 3 CT cases under the `relationships` group: +- `add_relationship_stamps_user_avps` +- `add_relationship_avps_are_per_direction` +- `add_relationship_default_avps_empty` + +--- + +## M7. Template support — RESOLVED + +**Spec:** §7 — *"A **template** is a named semantic context defined on +a class in the ontology. ... Not a blank form waiting to be filled — +it is an active node in the ontology."* + +**Status:** Substantively landed during the H-task series alongside +the Connection-arc and Template-AVP work. What landed: + + - 5th node kind: `kind = template`. Validated by + `graphdb_bootstrap:kind_order/1` (template = 5). + - Bootstrap node 31 — `Template` AVP-marker attribute, parented to + nref 16 (Instance Relationships). Stamped with + `relationship_avp => true` post-bootstrap. + - **Per-class templates**: templates are written as compositional + children of their owning class. Each `create_class/2` automatically + attaches a `"default"` template; class authors may `add_template/2` + more, or delete the default to force explicit template specification + on every connection arc. + - Public API on `graphdb_class`: `add_template/2`, `get_template/1`, + `templates_for_class/1`, `default_template/1`, + `class_in_ancestry/2`. + - Template-scoped `add_relationship` on `graphdb_instance`: `/4` + resolves the source class's default template; `/5` accepts an + explicit `TemplateNref`; `/6` adds per-direction user AVPs (M5). + Template AVP `#{attribute => 31, value => TemplateNref}` is stamped + at index 0 of each connection row's AVP list. Out-of-scope templates + produce `{error, {template_class_not_in_ancestry, ...}}`. + +Tests: `graphdb_class_SUITE` templates group (7 cases), +`graphdb_instance_SUITE` connection-arc cases (4 cases), +`graphdb_attr_SUITE` seeding group (2 cases), +`graphdb_bootstrap_tests.erl` kind_order cases. + +--- + +## M8. Attribute "type" implied by parent subtree — RESOLVED + +**Status:** Fixed via AVP-based marker. `graphdb_attr` seeds a fourth +runtime literal attribute, `attribute_type`, alongside `literal_type`, +`target_kind`, and `relationship_avp`. All `create_*` paths stamp an +`#{attribute => attribute_type_nref, value => name|literal|relationship}` +AVP on the new node. Bootstrap attribute nodes (nrefs 6–31) are +retro-stamped at `graphdb_attr:init/1` by walking the `parents` cache. + +New public API: `graphdb_attr:attribute_type_of/1` returns +`{ok, name | literal | relationship}` directly from the AVP. + +Tests: 10 CT cases under the `attribute_type` group. + +--- + +## F1. Language Ontology Bootstrap — RESOLVED + +Gate: must land before F2. `TheKnowledgeNetwork.md` §15 now documents +Languages as any communication form with grammar, syntax, and tokens or +icons — significantly broader than human natural languages alone. Four +top-level categories belong under the Languages node (nref 4): + +**Status:** Complete. Nrefs 32–35 seeded in `bootstrap.terms`. CT +coverage in `graphdb_bootstrap_SUITE` (`load_language_subcategories`). + +- Human Languages — written and verbal natural languages +- Formal Languages — programming languages, query languages, + mathematical notation +- Diagram Languages — UML, engineering schematics, tabular forms, + hierarchical diagrams +- Renderers — shared rendering engines (also: views) + +The current `bootstrap.terms` has no named subcategories under nref 4. +This task adds them, updates all dependents, and resolves any code +implications before F2 coding begins. + +### Planning step + +Output: nref assignments + any new sub-tasks appended to Engineering +Hygiene. Audit before writing code: + +1. All code that references nref 4 or the Languages subtree by nref + constant — note what each piece needs. +2. All CT assertions on exact bootstrap node/arc counts — these will + need updating (+4 nodes, +8 arcs minimum). +3. Final nref assignments for the four new category nodes (candidates: + 32–35; confirm no conflicts with existing bootstrap or seeded nrefs). +4. Any deferred follow-up tasks surfaced — append to Engineering + Hygiene below. + +Record nref assignments in the CLAUDE.md Bootstrap Nref +Quick-Reference table and cerebrum.md before writing any code. + +### Execution + +1. `apps/graphdb/priv/bootstrap.terms` — add four category nodes and + eight compositional arc rows (two per parent/child pair, connecting + each new node to Languages nref 4). Arc labels: ChildArc=22, + ParentArc=21; `kind=composition` — same pattern as all other + category arcs. + +2. `CLAUDE.md` — update Bootstrap Nref Quick-Reference table with the + four new entries. + +3. CT suites asserting exact node/arc counts — update expectations. + +4. This file (F2, M6-B and M6-D) — update English concept node seeding + target to Human Languages (assigned nref), not nref 4 directly. + +**Dependencies:** none upstream. Gates F2. + +--- + +## F2. M6 — Multilingual Layer — RESOLVED + +**Status:** Complete. `graphdb_language` is a fully implemented gen_server. +24/24 CT tests pass (`graphdb_language_SUITE`). 192/192 CT total, 99 EUnit, +zero warnings. M6-I (write-path integration) is explicitly deferred — it +depends on L4 (wire `graphdb_mgr` write-side). All Architecture Review issues +R1–R10 are resolved or closed. See Decision Log below. + +**Depends on F1.** + +**Spec:** §15 — *"Concepts are stored language-neutrally in the +ontology. Labels, prompts, and vocabulary entries are stored per +language and swapped at rendering time without modifying the +knowledge."* (§15 > Human Languages) + +**Design:** The environment node record (`#node{}`) is unchanged and +the environment database is the English default — the terminal fallback +for every language chain. English is the practical common language of +international communication; it is acknowledged as the environment's +base language without apology. Language-specific labels are stored in +per-language Mnesia tables within the same schema (overlay model). A +language chain is a runtime parameter scoped to the session, user, or +use case; resolution walks the chain left-to-right and falls through to +the environment node on miss. Per-AVP override semantics: a language +overlay record carries only the AVPs it overrides; all other AVPs +resolve from the environment node unchanged. + +Dialect distinctions are optionally supported. A dialect code such as +`en_gb` or `pt_br` (atom, underscore convention) identifies a +finer-grained overlay table. Dialect overlays carry only terms that +genuinely differ from the base language; most terms fall through to the +base language or the environment. Dialectal variants are an explicit +authoring decision — the system never infers which dialect a string +belongs to. + +Project databases mirror this model. The terminal fallback is the +project node record, authored in an implementer-chosen language. That +language is specified as an AVP on the project root node, referencing a +language concept node in the environment. + +**Completed state:** `graphdb_language.erl` is a full gen_server +implementation covering M6-A through M6-H and M6-J. `bootstrap.terms` +carries English strings as node AVPs — these are the English default and +require no migration. M6-I is deferred to L4. + +--- + +### Sub-tasks + +> **Pre-implementation gate:** Blockers R1–R4 in the Architecture +> Review section below must each have a recorded decision before any +> sub-task here is coded. R1 (project nref collision) and R2 (dialect +> algorithm) affect API signatures and test cases; resolving them first +> prevents cascading rework. + +**M6-A: Language overlay record and Mnesia schema** + +```erlang +-record(language_node, { + nref, %% integer() — same keyspace as environment nodes table + avps %% [#{attribute => AttrNref, value => Value}] + %% — AVPs that shadow matching AVPs on the environment node +}). +``` + +One Mnesia `disc_copies` table per language or dialect +(`language_en`, `language_de`, `language_en_gb`, `language_pt_br`, …) +with `{record_name, language_node}`. Tables are created on demand when +a language or dialect is registered. `graphdb_bootstrap` creates +`language_en` at environment init; it will be mostly empty in practice +since the environment node record is itself the English default — the +table exists to make `en` a well-formed chain entry. + +> **R6 (should fix):** Specify runtime `mnesia:create_table/2` +> behaviour in `register_language/2` and `register_dialect/3` — +> synchronous vs. async, timeout, concurrent-registration safety across +> nodes. See Architecture Review. + +**M6-B: Language concept nodes** + +`graphdb_language:init/1` seeds a language concept node for English +under Human Languages (nref 32) using the standard +ensure-seed-by-name pattern. **Node kind: `instance`.** English is +already bootstrapped as `kind=instance` at nref 10000 (F2); all +language nodes (base languages, dialects) follow the same kind. +`kind=instance` eliminates the dual-mechanism risk: instances do not +participate in taxonomic IS-A arcs, so `base_language` AVP is the +sole authority for base/dialect relationships. The English nref is +cached in gen_server state and exposed via +`graphdb_language:seeded_nrefs/0`. + +Base languages and dialects are both language concept nodes, but +dialect nodes carry an AVP that references their base language concept +node: + +```erlang +#{attribute => base_language_nref, value => BaseLanguageConceptNref} +``` + +`base_language` is seeded as a literal attribute in +`graphdb_language:init/1` (same seeding pattern as `target_kind`). +Base language nodes carry no such AVP. This makes the base/dialect +relationship explicit, queryable, and independent of the atom naming +convention. + +> **R3: RESOLVED** — `kind=instance` for all language nodes. +> See Decision Log. +> +> **R4 (should fix):** Move `project_language` seeding from +> `graphdb_attr:init/1` to `graphdb_language:init/1` — owning worker +> pattern. See Architecture Review. + +**M6-C: Label resolver** + +```erlang +graphdb_language:resolve_label(Nref, AttrNref, Chain) -> Value | not_found +``` + +`Chain :: [atom()]` — language code atoms in priority order +(e.g., `[de, en_gb, en, fr]`). Walk: for each code in the chain: + + - If the code equals the environment's declared language (`en` by + default, readable from `graphdb_language:seeded_nrefs/0`): skip + the overlay table lookup and fall directly to the terminal node + read. This makes `en` a zero-cost sentinel — `language_en` is not + read, and the environment node record is used immediately. + - Otherwise: read `language_` table for Nref; if a record + exists and its `avps` contains AttrNref, return that value. + +If the chain is exhausted without a match, read from the terminal node +table (environment `nodes`, or project `nodes` for project nrefs). If +still absent, return `not_found`. + +For project nref resolution, the caller passes the project `nodes` +table name as an explicit terminal parameter — the resolver has no +global state about which database owns a given nref. + +> **R1 (blocker):** Signature is missing the terminal-table parameter, +> and project nrefs share the same integer keyspace as environment +> nrefs — a single `language_*` table keyed by nref alone cannot +> distinguish them. Resolve the project-side overlay story (shared vs. +> per-project tables, key scheme) and update the signature before +> coding. See Architecture Review. +> +> **R8 (should fix):** Specify where the environment's declared +> language code is stored (config, AVP, constant) — the sentinel +> optimisation depends on this lookup being authoritative and fast. +> See Architecture Review. + +**M6-D: Language registration** + +```erlang +%% Base language +graphdb_language:register_language(Code :: atom(), Name :: string()) + -> {ok, Nref} | {error, already_registered} | {error, _} + +%% Dialect — must name an already-registered base language +graphdb_language:register_dialect(Code :: atom(), Name :: string(), + BaseCode :: atom()) + -> {ok, Nref} | {error, base_not_found} + | {error, already_registered} + | {error, _} +``` + +Both create the concept node under Human Languages (nref 32) and its Mnesia overlay table. `register_dialect/3` additionally +stamps the `base_language` AVP on the dialect node, referencing the +base language concept nref. Calling `register_dialect/3` with an +unregistered `BaseCode` is an error. Both calls are idempotent on +restart (seed-by-name pattern). + +> **R6 (should fix):** Specify runtime `mnesia:create_table/2` +> behaviour — synchronous, timeout, concurrent-registration safety. +> See Architecture Review. + +**M6-E: Overlay write** + +```erlang +graphdb_language:set_labels(Nref, Code :: atom(), AVPs) -> ok | {error, _} +``` + +Writes or merges AVPs into the language overlay record for Nref in +`language_`. Merge semantics: existing AVPs for other attributes +on the same record are preserved; only the supplied AttrNrefs are +updated or added. + +**M6-F: Translation agent hook** + +```erlang +graphdb_language:register_translation_hook(Fun) -> ok +%% Fun :: fun((Nref :: integer(), DefaultAVPs :: [avp()]) -> ok) +``` + +Called after environment node creation with the new nref and its +English AVPs. Initially the hook list is empty (silent no-op). Multiple +hooks accumulate in registration order; all are called post-commit. +This is the designed insertion point for a future LLM-based translation +agent. As language overlays accumulate, translation patterns may emerge +and be encoded as rules — the hook is the path through which that +learning is initiated. + +> **R7 (should fix):** Hook must be invoked in a spawned process +> (`proc_lib:spawn/1`), never inline — inline blocks all callers and +> crashes the worker on exception. Add `unregister_translation_hook/1` +> for test cleanup. Clarify return-value contract (currently +> unspecified). See Architecture Review. + +**M6-G: Project default language** + +Seed `project_language` literal attribute in `graphdb_attr:init/1` +alongside `target_kind`, `relationship_avp`, `attribute_type`, and +`literal_type`. The project root node carries: + +```erlang +#{attribute => project_language_nref, value => LanguageConceptNref} +``` + +Public API: + +```erlang +graphdb_language:project_language(ProjectRootNref) + -> {ok, Code :: atom()} | not_found +``` + +Reads the `project_language` AVP from the project root node and +returns the language code atom for the referenced concept node. + +> **R1 (blocker):** Project-side overlay story unresolved — see M6-C +> callout and Architecture Review. This API cannot be fully specified +> until the project nref keyspace collision is resolved. + +**M6-H: Session chain helper** + +```erlang +graphdb_language:make_chain(Codes :: [atom()]) -> [atom()] +``` + +Validates each code against registered languages; drops unknown codes +with a log warning. Applies the dialect auto-insertion rule using the +following verified pseudocode: + +``` +make_chain(InputCodes): + ValidCodes = [C || C <- InputCodes, is_registered(C)] + Output = [] + Remaining = ValidCodes + while Remaining != []: + Code = head(Remaining) + Remaining = tail(Remaining) + Output = Output ++ [Code] % always emit + if is_dialect(Code): % concept node has base_language AVP + Base = base_language_of(Code) % AVP nref → concept node → lang_code atom + FullChain = Output ++ Remaining % current output (incl. Code) + remaining input + if Base not in FullChain: + Output = Output ++ [Base] % insert base immediately after dialect + return Output +``` + +The check is `Base not in (Output ++ Remaining)` — the full current +chain view, not just the output built so far. A base that still +appears later in the remaining input is not re-inserted. + +Verified derivations: + + - `[de, en_gb, fr]` → `[de, en_gb, en, fr]` (en∉[de,en_gb,fr] → insert) + - `[en_gb, en_us]` → `[en_gb, en, en_us]` (en∉[en_gb,en_us] → insert after en_gb; + en∈[en_gb,en,en_us] → skip after en_us) + - `[en_gb, en, fr]` → `[en_gb, en, fr]` (en∈[en_gb,en,fr] → skip) + - `[pt_br, de]` → `[pt_br, pt, de]` (pt∉[pt_br,de] → insert) + +Implementation notes: +- `base_language_of/1` does two Mnesia reads: concept-node-by-code → + `base_language` AVP nref → concept-node-by-nref → `lang_code` atom. + Cache results within a single `make_chain/1` call. +- `is_dialect/1` is a check for the presence of `base_language` AVP + on the concept node — no separate flag needed. + +Callers do not construct Mnesia table names directly. + +> **R2: RESOLVED** — pseudocode verified against all four examples. +> See Decision Log. + +**M6-I: Write-path integration** *(DEFERRED to L4)* + +Depends on L4 (wire `graphdb_mgr` write-side operations). When the +NYI write operations (`create_attribute`, `create_class`, +`create_instance`) are implemented, each must: + + 1. Create the environment node atomically in one Mnesia transaction. + 2. Call all registered translation hooks post-commit with the new + nref and its English AVPs. (Outside the transaction — best-effort.) + 3. If a session language list is provided with labels, call + `set_labels/3` for each language. (Also outside the transaction.) + +Steps 2–3 are not atomically coupled to step 1 by design. A failed +hook or missing language label does not roll back node creation. + +Dialect write discipline: do not auto-duplicate environment labels into +dialect overlay tables. A dialect overlay record is only written when +the label genuinely differs from the base language. The session +language list declares the context for new labels; deciding whether a +term warrants a dialect-specific override is an explicit authoring +decision, never inferred by the system. + +> **R1 (blocker):** Write-path integration for project instances cannot +> be specified until the project-side overlay story is resolved — +> including whether project-instance labels go into shared or +> per-project overlay tables. See Architecture Review R1 and R13. + +**M6-J: Tests** + +EUnit (`graphdb_language_tests.erl`) — pure function coverage: + + - `make_chain/1`: unknown codes silently dropped; known codes + preserved in order. + - `make_chain/1`: dialect auto-insertion — base inserted when absent + from chain as built so far (base determined from concept node AVP, + not atom parsing). + - `make_chain/1`: multiple dialects of same base — single insertion. + - `make_chain/1`: base already present in chain — no duplicate. + - `make_chain([])` → `[]`. + +CT (`graphdb_language_SUITE.erl`) — integration: + + - Register language → overlay table created; idempotent on + re-register. + - Register dialect → concept node carries `base_language` AVP + referencing base concept nref; `base_not_found` when base + unregistered. + - `set_labels/3` → AVP readable via `resolve_label/3`. + - `set_labels/3` with unregistered code → error, no write. + - Fallback: no overlay record → resolves from environment node. + - Chain priority: first-listed language wins over second. + - `en` sentinel: chain containing `en` reads environment node + directly; `language_en` table is not consulted. + - Dialect hit: `en_gb` overlay record returned when present; falls + through to environment when absent. + - Dialect fallback chain: `[en_gb, en, fr]` — `en_gb` miss → `en` + sentinel → environment node (skips `fr` because terminal matched). + - Project language AVP written and retrieved correctly. + - Translation hook: registered `Fun` called on node creation; empty + list is a silent no-op. + - Translation hook crash during node creation: creation must succeed. + - Re-register an already-registered language with a different name: + decide (error or overwrite) and test. + - Dialect node whose `base_language` AVP references a missing + concept: graceful resolution path. + - Mnesia transaction abort during `set_labels/3`: caller sees error, + no partial write. + +**Dependencies:** F1 must land first. Must land before F3 — query +render-time label resolution depends on the language overlay API. + +--- + +### Architecture Review — Open Issues + +Post-design audit conducted before implementation. Each blocker must be +resolved (with a decision recorded in the Decision Log) before any M6 +code is written. Should-fix items should be resolved during the +relevant sub-task. Notes are informational and do not block. + +#### Blockers + +**R1. RESOLVED** — `resolve_label/4` with `Scope :: environment | {project, AnchorNref}`. +Environment tables: `language_`. Project tables: `language__`. +`overlay_table_name/2` encodes both forms. M6-I (write-path integration) depends on +L4 (wire graphdb_mgr write-side) and is explicitly deferred. See Decision Log. + +**R2. RESOLVED** — Pseudocode verified in M6-H. The check is +`Base not in (Output ++ Remaining)` (full chain view). See Decision +Log. + +**R3. RESOLVED** — `kind=instance` for all language nodes. See +Decision Log. + +**R4. RESOLVED** — `project_language` seeded by +`graphdb_language:init/1`. Owning-worker pattern confirmed. See +Decision Log. + +#### Should Fix + +**R5. RESOLVED** — Environment stores English strings directly on +`#node{}` records (name AVPs on environment nodes). Documented +departure from the strict reading of §15. Rationale: English is the +environment's base language; reading it directly from the node record is +zero-overhead and the en sentinel in `do_resolve_chain/4` makes this +explicit by design, not accident. See Decision Log. + +**R6. RESOLVED** — `mnesia:create_table/2` called synchronously from +`ensure_overlay_table/1` during `register_language/2` and +`register_dialect/3`. The gen_server serialises all callers; no +concurrent registration races within a single node. Default Mnesia +timeout applies. Multi-node schema propagation is a known future +concern (R12 tracks table-count ceiling); acceptable for the +current single-node deployment model. See Decision Log. + +**R7. RESOLVED** — Hooks spawned via `proc_lib:spawn/1`; never inline. +Each hook body wrapped in try/catch; errors logged and discarded; +never propagated to the caller. `unregister_translation_hook/1` +added for test cleanup. See Decision Log. + +**R8. RESOLVED** — Environment language code stored as compile-time +macro `?ENV_LANGUAGE_CODE = en` in `graphdb_language.erl`. Exposed +via `seeded_nrefs/0` as `env_language_code => en` so callers and +tests can read it without a magic atom. See Decision Log. + +#### Notes + +**R9. Test coverage gaps resolved in M6-J above.** The cases missing +from the original spec have been folded into the M6-J test list. + +**R10. RESOLVED** — `en_gb` (underscore-separated lowercase atom) +chosen over IETF BCP 47 `en-GB`. Rationale: Erlang atoms cannot +contain unquoted hyphens; requiring quoted atoms (`'en-GB'`) would +make API usage awkward. Underscore-lowercase is idiomatic in Erlang. +The convention is documented; not a format bug. See Decision Log. + +**R11. No batch resolver API.** `resolve_label/3` is per-AVP. A +future `resolve_labels(Nref, [AttrNref], Chain) -> #{AttrNref => Value}` +will be wanted at F3 render time. Note for F3 planning; not blocking +M6. + +**R12. Mnesia table proliferation ceiling.** Default Mnesia schema +supports ~1024 tables. ISO 639 base codes (~200) plus dialects could +approach that in a fully-internationalised deployment. Most deployments +stay well under 20. Fallback design is a single `language_overlays` +table keyed by `{Code, Nref}`. Revisit if a deployment approaches the +ceiling. + +**R13. Project-side overlays absent from write-path plan.** *(M6-I)* + +The plan covers project *terminal fallback* but does not specify how +project-instance labels are written into overlay tables or retrieved. +Overlaps with blocker R1 — resolving R1 should also answer this. + +**R14. Snapshot consistency during render.** Multiple sequential +`resolve_label/3` calls while a concurrent `set_labels/3` is mid-flight +can return a mix of old and new values. Acceptable for labels; document +in the Decision Log so it is not later misconstrued as a correctness +bug. + +**R15. Translation hook return value contract undefined.** *(M6-F)* + +The signature `Fun :: fun((Nref, DefaultAVPs) -> ok)` implies the +return is always `ok`, but this is not enforced. Document explicitly +that the return value is discarded, or change the contract to +`-> ok | {error, Reason}` and specify what happens on error. + +#### Decision Log + +**R1 — Scope type: `environment | {project, AnchorNref}`** (2026-05-18) + +`resolve_label/4` takes a `Scope` argument distinguishing environment +reads (table `language_`) from project reads (table +`language__`). M6-I (project write-path) deferred +to L4; the scope type is already in the public API so the boundary is +clean when L4 lands. + +**R2 — Dialect auto-insertion uses full chain view** (2026-05-18) + +`do_make_chain/3` checks `Base not in (Output ++ Remaining)` — +the full chain view, not just the output so far. This ensures a base +already scheduled to appear later in the chain is not inserted early +and duplicated. Verified by hand-tracing `[en_gb, en_us]` with +`en_gb=>en, en_us=>en`. + +**R5 — English on env node records, not overlay table** (2026-05-18) + +English strings live on `#node{}` `attribute_value_pairs` fields, not +in a `language_en` Mnesia table. The `en` sentinel in +`do_resolve_chain/4` bypasses the overlay lookup and reads the node +directly. Rationale: zero-overhead for the most common case; avoids +duplicating every English label into a separate table at bootstrap. +This is a deliberate departure from the strict reading of §15 +("language-neutral storage") — English is the structural language of +the environment and is treated specially by design. + +**R6 — Synchronous overlay table creation, single-node only** (2026-05-18) + +`mnesia:create_table/2` is called synchronously inside the gen_server +handler for `register_language/2`. The gen_server serialises all +callers so there is no concurrent-registration race within a node. +Multi-node schema distribution is a known future concern deferred to +whenever multi-node support is added; for now the single-node model +is the only deployment target. + +**R7 — Hooks spawned, crash-safe, unregister for tests** (2026-05-18) + +Translation hooks are invoked via `proc_lib:spawn/1` so a slow or +crashing hook cannot block or kill the gen_server. Each hook body is +wrapped in try/catch; errors are logged and discarded. The return +value is always discarded. `unregister_translation_hook/1` exists +specifically so CT cases can clean up their hooks between test cases. + +**R8 — Environment language code as compile-time macro** (2026-05-18) + +`?ENV_LANGUAGE_CODE = en` is a module-level macro. Exposed through +`seeded_nrefs/0` as `env_language_code => en` so callers can read it +without an atom literal. Chosen over a config parameter because the +environment language is a structural invariant, not a deployment +setting — changing it would require re-bootstrapping the entire +environment. + +**R10 — Underscore-lowercase atom convention for locale codes** (2026-05-18) + +`en_gb` rather than `'en-GB'`. Erlang atoms containing hyphens must +be quoted; unquoted `en_gb` is idiomatic and avoids the quoting +requirement. All locale codes in the API follow this convention. +Applications bridging to BCP 47 external systems must translate at +the boundary. + +--- + +## F3. graphdb Query Language — RESOLVED + +Implemented as `graphdb_query` (the `graphdb_language` slot is occupied +by the M6 multilingual overlay layer). Design at +`docs/designs/f3-graphdb-query-design.md`; plan at +`docs/superpowers/plans/2026-05-23-f3-graphdb-query.md`. Seven query +primitives (Q1, Q1b, Q2-Q6), snapshot-semantics sessions, continuation ++ resume with `snapshot_expired` detection. Template-filtered +traversal lands in a future iteration alongside richer query criteria. + +--- + +## F4. E1 — `graphdb_rules` Rule Engine + +**Can start after F1. Parallel to F3 at discretion — E1 is large +scope; serial execution (F3 then F4) is a reasonable alternative.** + +**Spec:** §8 (rules as stored data), §9 (instantiation engine), §10 +(composition rules), §11 (reactive learning). + +The design splits E1 into six phases (A–F). See +`docs/designs/f4-graphdb-rules-design.md`. + +### F4 Phase A — Rule data model — **RESOLVED** (2026-06-02) + +`graphdb_rules` replaced its stub with the Phase A data model: a +runtime-seeded rule meta-ontology (`Rule` abstract class + +`CompositionRule` / `ConnectionRule` under nref 3; `Rule Literals` +sub-group + 6 literal attrs under nref 7; `applies_to` / `applied_by` +relationship-attribute pair under nref 16), and a scope-aware +create/retrieve API: `create_composition_rule/6,7`, +`create_connection_rule/7,8`, `get_rule/2`, `rules_for_class/2`, +`composition_rules_for_class/2`, `connection_rules_for_class/2`, +`list_rules/1`, `seeded_nrefs/0`. Content AVPs live on the rule +instance node; deployment AVPs (`mode`, `multiplicity`, `Template`) on +the `applies_to` connection arc. Retrieval is direct-attachment only. +`graphdb_rules` moved to the last child of `graphdb_sup`. 37 CT cases +added (`graphdb_rules_SUITE`). + +### F4 Phase B — Rule-firing engine (composition + connection) + +Phase B builds the engine that consumes the Phase A data model. It was +divided into independently-shippable pieces; the firing engine itself — +taxonomy-walk reads, composition firing, propose mode, the +multiplicity-range refactor, and connection firing — landed here. +Conflict precedence and the later phases (instantiation engine, reactive +learning) remained outstanding and are tracked in `TASKS.md`. + +The landed pieces (each with its own design + plan): + +- **B1 — `effective_rules_for_class/2` (read-side taxonomy walk) — DONE.** + Nearest-first gather of every rule attached to a class and its taxonomy + ancestors, grouped by attaching class, each paired with its + `applies_to`-arc deployment (`mode`/`multiplicity`/`template`). Resolves + nothing — additive-vs-shadow is the firing engine's job. Design: + `docs/designs/f4-phase-b1-effective-rules-design.md`. +- **B2 — composition firing engine — DONE.** `create_instance/3` fires + `mandatory` rules inside the parent-creation transaction and `auto` rules + post-commit. Return shape is `{ok, Nref, Report}` / `{error, Reason, + Report}` (3-tuple on firing path). `plan_composition_firing/2` is a + pure-read helper reused by B3. Design: + `docs/designs/f4-phase-b2-composition-firing-design.md`. +- **B3 — `propose` mode — DONE.** `propose`-mode composition rules + materialise nothing; they surface as `proposed` outcomes in the + `create_instance/3` report (always-in-report — no session flag). A + caller accepts a proposal by issuing an ordinary `create_instance/3` + for the proposed class. Design: + `docs/designs/f4-phase-b3-propose-mode-design.md`. +- **B-prep — multiplicity-range refactor — DONE (PR #36).** Reshaped + `multiplicity` from `pos_integer() | unbounded` to a + `{Min, Max}` pair (`Min :: non_neg_integer()`, `Max :: pos_integer() | + unbounded`) across **both** composition and connection rules — uniform + rule shape; `unbounded` survives only as a value of `Max`, never + standalone, so deployment carries `{Min, Max}`. `mode` enforces the + floor (`mandatory` ⇒ ≥ `Min`); `Max` caps. Touches Phase A + `create_*_rule` signatures + validation, B1 `decode_deployment`, B2 + `plan_mandatory` / `expand_children`, and the CT suites (greenfield — + test churn only). Composition mint-from-range is **decided**: firing + mints `Min` (the floor drives the count); `Max` is the ceiling for a + future *interactive creation session* (human or autonomous agent tops up + optional children up to `Max`) — a separate later feature. Must land + before B4 (which consumes `{Min, Max}` deployment). See + `docs/designs/f4-phase-b4-connection-firing-design.md` §7 and B4-D5. +- **B4 — connection firing — DONE.** `create_instance/4` threads a + caller-supplied resolver; a RESOLVE step fires effective ConnectionRules + (`mandatory` in the root txn, `auto` post-commit, `defer`/`propose` + reported). ConnectionRule gains a `reciprocal_nref` content AVP; + `create_connection_rule/8,9` (reciprocal param) supersede `/7,8`; + `effective_connection_rules/2` is the read seam. Design + `docs/designs/f4-phase-b4-connection-firing-design.md`; plan + `docs/superpowers/plans/2026-06-11-f4-phase-b4-connection-firing.md`. +**Evidence:** `apps/graphdb/src/graphdb_rules.erl` and +`apps/graphdb/src/graphdb_instance.erl` carry Phases A + B1–B4. + +--- + +## Engineering Hygiene + +No blocking dependencies on any feature phase. Interleave at any point. + +--- + +### L1. Rename `inherited_attributes/1` → `inherited_qcs/1` — RESOLVED (subsumed by L2) + +**Evidence:** `graphdb_class.erl:230-238, 638-651`. + +The function returns qualifying-characteristic *attribute nrefs* from +the class and its ancestors — not inherited *values*. The name +`inherited_attributes` implies §6 value inheritance, which is +different. + +**Fix:** rename to `inherited_qcs/1`. Reserve `inherited_attributes` +for §6 semantics if/when class-level bound-value inheritance is exposed +as its own API. + +--- + +### L2. Unify QC declarations and class-bound values into a single AVP shape — RESOLVED + +**Evidence:** `graphdb_class.erl:524-562, 863-908, 1001-1040`. +`do_add_qc` currently writes a sentinel-keyed AVP +`#{attribute => QcAttrNref, value => AttrNref}` to record that +`AttrNref` is a qualifying characteristic. Class-bound values (e.g. +`#{attribute => ColorAttrNref, value => red}`) share the same AVP list +but use a different key. `resolve_from_class` in `graphdb_instance` +must avoid confusing the two, and adding more concept tags in F4 would +make the list harder to reason about. + +**Fix:** replace the sentinel-keyed pattern with a unified shape: + +- **QC declared, no bound value:** `#{attribute => AttrNref, value => undefined}` +- **QC with class-level bound value:** `#{attribute => AttrNref, value => SomeValue}` + +Both forms are normal AVPs keyed by the actual attribute nref. Adding a +QC writes `undefined`; binding a class value updates the entry (or +writes it if not yet declared). `resolve_from_class` skips +`value = undefined` entries — they are schema declarations, not +resolved values. Inheritance walk collects all unique `attribute` keys +nearest-first, carrying `{AttrNref, Value | undefined}` pairs. + +**Changes required:** + +1. Remove the seeded `qualifying_characteristic` literal attribute and + `qc_attr_nref` from `graphdb_class` state — no longer needed. +2. `do_add_qc/3` writes `#{attribute => AttrNref, value => undefined}`. + Idempotent: if the key already exists (any value), leave it alone. +3. `inherited_attributes/1` → `inherited_qcs/1` (L1 rename, fold in + here). Return type changes from `[AttrNref]` to + `[{AttrNref, Value | undefined}]`, deduplicating by `AttrNref` with + nearest-ancestor priority. +4. `collect_all_qcs/2` and `collect_qc_nrefs/2` simplified to a fold + over all AVPs with dedup by `attribute` key. +5. `search_class_taxonomy` in `graphdb_instance.erl` — guard + `value =/= undefined` before treating an AVP as a resolved hit. + +**Deferred:** instance-only enforcement (attributes that must never +receive a class-level value) belongs in the template attribute list, +which does not yet exist. The `undefined` shape accommodates this +naturally — an instance-only attribute stays `undefined` at every class +level. Enforcement is a follow-on task adjacent to L4/F4. + +**Note:** best done before F4 (E1) starts adding more concept tags to +class nodes. Subsumes L1 (`inherited_attributes/1` → `inherited_qcs/1`). + +--- + +### L3. Single-row reads run inside `mnesia:transaction/1` — RESOLVED + +**Evidence:** `graphdb_class.erl:506, 569-575, 601-611`, +`graphdb_instance.erl:393, 406, 453-459, 486, 499`, +`graphdb_mgr.erl:357`. + +**Fix:** use `mnesia:dirty_read/2` for read-only single-row lookups +that don't need transactional isolation. Reserve transactions for +multi-row writes and reads that must observe atomic state. + +--- + +### L4. Wire `graphdb_mgr` write-side to workers — RESOLVED + +**Evidence:** `graphdb_mgr.erl:278-296`. `create_attribute`, +`create_class`, `create_instance`, `add_relationship` all return +`{error, not_implemented}` despite the workers being fully functional. + +The spec's organizing claim is that `graphdb_mgr` is the single public +entry point. Today that is true only for reads. *Higher impact than +others in this section — restores the spec's public API contract.* + +**Fix:** delegate each handler to the corresponding worker: +- `create_attribute` → `graphdb_attr:create_*` (route by kind) +- `create_class` → `graphdb_class:create_class/2` +- `create_instance` → `graphdb_instance:create_instance/3` +- `add_relationship` → `graphdb_instance:add_relationship/4` +- `delete_node`, `update_node_avps` → category guard, then + kind-appropriate worker. + +**Design note — attribute categories per class context:** + +When wiring `create_class` and `update_node_avps`, the write-side must +account for two categories of attributes that a class declares: + +- **Class-bindable** — the class may supply a value (or a useful + default) for this attribute. Instances inherit the value and may + override it. Example: `num_wheels = 4` on a Car class. +- **Instance-only** — the class declares the attribute as relevant but + binding a value at the class level is a category error. The value is + meaningful only per-instance. Example: `serial_number`, `owner_name`. + Attempting to bind a class-level value for such an attribute should be + rejected or flagged. + +This distinction is **per-class, per-template context** — the same +attribute may be class-bindable in one class's template and instance-only +in another's. The enforcement point belongs in the template's attribute +declaration, not on the attribute node globally. + +The template attribute list does not yet exist (templates currently carry +only a name and their compositional arc). L4 implementation should treat +this as a known gap: wire the delegation first, then plan the template +attribute list and instance-only enforcement as a follow-on task (likely +adjacent to F4/E1, which adds rule-driven instantiation). Document the +gap in the Decision Log when L4 lands. + +#### Decision Log + +**L4 — `create_attribute` routing by ParentNref** (2026-05-18) + +`graphdb_mgr:create_attribute/3` routes to the appropriate +`graphdb_attr` worker function based on `ParentNref`: +- 6 / 9–12 (Names subtree) → `create_name_attribute/1` +- 7 (Literals) → `create_literal_attribute/2`; `type` extracted from `AVPs` map (default `string`) +- 8 / 13–16 (Relationships subtree) → `create_relationship_attribute/3` if both + `reciprocal_name` and `target_kind` present; `create_relationship_type/1` if neither; + `{error, {missing_avps, ...}}` if exactly one is present +- Unknown parent → `{error, {unknown_attribute_parent, Nref}}` + +`create_relationship_attribute/3` returns `{ok, {FwdNref, RevNref}}`; the mgr +normalises to `{ok, FwdNref}` (forward arc nref only). + +**L4 — Instance-only attribute enforcement deferred** (2026-05-18) + +The template attribute list (which would declare per-class, per-template whether an +attribute is class-bindable or instance-only) does not yet exist. `create_class` +and `update_node_avps` accept any AVP write without enforcement. This is a known +gap; enforcement is a follow-on task adjacent to F4/E1 (rule-driven instantiation). + +**L4 — `delete_node` and `update_node_avps` remain `not_implemented`** (2026-05-18) + +No worker currently implements node deletion or general AVP-update. Both operations +pass through the category guard (rejecting category nrefs 1–5) and then return +`{error, not_implemented}`. These will be wired when a worker adds the functionality. + +--- + +### L5. Relationship row IDs allocated from the global `nref_server` — **RESOLVED** (2026-05-19) + +New `rel_id_server` gen_server added to `apps/graphdb/src/` as first child of +`graphdb_sup`. All 23 `#relationship.id` allocations across 5 files migrated from +`nref_server:get_nref/0` to `rel_id_server:get_id/0`. Bootstrap test assertions +updated (nref floor now `>= 100002`; relationship IDs now start at 1). 4 CT tests added. + +--- + +### L7. Literals subtree restructuring — **RESOLVED** (2026-05-25) + +Literals subtree (nref 7) partitioned by owning subsystem so each +worker seeds its literal attributes under a dedicated sub-group: + +- `Attribute Literals` — seeded by `graphdb_attr:init/1` (contains + `literal_type`, `target_kind`, `relationship_avp`, `attribute_type`) +- `Language Literals` — seeded by `graphdb_language:init/1` (contains + `base_language`, `project_language`) +- `Rule Literals` — seeded by `graphdb_rules:init/1` once F4 Phase A + lands + +`graphdb_attr:create_literal_attribute/3` arity added so callers can +specify a parent nref. `/2` retained as a delegating shim defaulting +to nref 7. + +Clean-slate seeding; no runtime migration code. + +--- + +### L8. Generalize `graphdb_attr` attribute placement — **RESOLVED** (2026-05-31) + +Parent nref is now a first-class, validated argument on every +`graphdb_attr` creator. Canonical general creators +`create_value_attribute/4` (single node) and +`create_relationship_attribute_pair/4` (reciprocal pair) back thin named +wrappers that preserve the default parents (6/7/8). `validate_parent/1` +rejects a non-existent or non-`attribute` parent before any write. +`create_relationship_attribute` renamed to +`create_relationship_attribute_pair`. Design at +`docs/designs/l8-graphdb-attr-placement-design.md`. Removes the F4 §10.1 +P1 placement blocker by construction. + +--- + +### L9. Non-instantiable (abstract) classes — **RESOLVED** (2026-06-01) + +A class may be designated non-instantiable (abstract) by an +`instantiable => false` marker AVP on the class node. `graphdb_attr` +seeds the `instantiable` boolean marker literal attribute in the +`Attribute Literals` sub-group. `graphdb_class:create_class/3` takes an +initial AVP list (`/2` delegates with `[]`); a class created with the +marker is born **without** a default template. `graphdb_class:is_instantiable/1` +reports the flag. `graphdb_instance:create_instance/3` **and** +`add_class_membership/2` refuse a non-instantiable class target with +`{error, {class_not_instantiable, ClassNref}}`. Permissive by default — +absence of the marker means instantiable. Design at +`docs/designs/l9-non-instantiable-classes-design.md`. Prerequisite for +F4 Phase A (Decision D15), which seeds the abstract `Rule` meta-class +root. + +--- + +### Task 7. Wire `dictionary_server` and `term_server` to `dictionary_imp` — **RESOLVED** (2026-05-19) + +Both gen_servers delegate to `dictionary_imp` via `start_dictionary/stop_dictionary` +in `init/terminate` and forward all CRUD calls. Also fixed a pre-existing one-line bug +in `dictionary_imp:delete/2` (wrong ETS key type). 14 CT tests added (7 per server). + +--- + +### Task 8. Scaffold nref constants → shared `graphdb_nrefs.hrl` header — **RESOLVED** (2026-05-20) + +`apps/graphdb/include/graphdb_nrefs.hrl` introduced with 36 named macros covering +scaffold nrefs 1–35 (`NREF_*`, `NAME_ATTR_*`, `ARC_*`) and the permanent English +instance nref 10000 (`NREF_ENGLISH`). All inline `-define` blocks removed from five +source files (`graphdb_attr`, `graphdb_class`, `graphdb_instance`, `graphdb_language`, +`graphdb_mgr`); all raw integers 17–35 and 10000 replaced with macros in seven test +files. Companion `graphdb_nrefs.erl` exports `scaffold_spec/0` and `verify/0`; verify +is called at the end of `graphdb_bootstrap:do_load/0` as a fatal congruency check. +`graphdb_bootstrap` module is deleted+purged from the code server in `graphdb_mgr:init/1` +after successful load. 2 CT tests in `graphdb_nrefs_SUITE`. 320 tests (217 CT + +103 EUnit), all green, zero warnings. + +--- + +### E2. Non-normal OTP start types — **RESOLVED** (2026-05-21) + +`seerstone:start/2` and `nref:start/2` now delegate `{takeover, Node}` and +`{failover, Node}` to the normal start path rather than hitting `?NYI`. +Full distributed takeover/failover semantics deferred until a distributed +deployment is planned. + +--- + +### E5. Replace `included_applications` with peer-app dependencies — **RESOLVED** (2026-05-21) + +**Evidence:** `apps/database/src/database.app.src` declares +`included_applications: [graphdb, dictionary]`. This is Dallas's 2008 +OTP idiom; modern OTP discourages it because included apps lose +independent restart, code reload, and application-callback semantics. +The `seerstone`↔`database` boundary was already modernized (2026-05-09); +this applies the same treatment one level deeper. + +**Fix:** + +1. Remove `included_applications: [graphdb, dictionary]` from + `database.app.src`. Add `graphdb` and `dictionary` to a higher-level + `applications:` dependency list. +2. Drop `graphdb_sup` and `dictionary_sup` from `database_sup:init/1`. +3. Decide whether `database` itself remains an OTP application. +4. Update `Architecture.md` §5 and supervision-tree diagrams in + `CLAUDE.md` files. + +**Note:** best done before E3 and E2, since `included_applications` +complicates both hot upgrades and distributed-app semantics. diff --git a/docs/resiliency-notes.md b/docs/resiliency-notes.md index 08f975d..9158b1a 100644 --- a/docs/resiliency-notes.md +++ b/docs/resiliency-notes.md @@ -2,7 +2,7 @@ Notes-only. Not a design, not an article. Just enough to remember the threads and pick them back up later. Started 2026-05-27 after pausing -the F4 Phase A seeding-shape discussion. +the rules-engine seeding-shape discussion. ## Threads to revisit @@ -39,8 +39,8 @@ the F4 Phase A seeding-shape discussion. Neither is decided. (A) is more invasive but removes the migration class of problems entirely. (B) is cheaper today. -**Important:** (A) vs (B) does not block F4 Phase A. F4 Phase A and -the small commit can land under either framing. +**Important:** (A) vs (B) does not block the rules data model. That +work and the small commit can land under either framing. ### 4. Brainstorm leftovers (R1–R6) @@ -55,11 +55,12 @@ brainstorm but never answered. Most are smaller than (A)-vs-(B): - **R2.** ~~Where do `applies_to` / `applied_by` arc-label nodes land?~~ **Resolved 2026-06-01:** under **nref 16** (Relationships > Instance Relationships, `?NREF_INST_REL_ATTRS`) — candidate (a). No - dedicated Rule Relationships sub-bucket. Recorded as F4 design D13 / - §10.1 P1 RESOLVED. + dedicated Rule Relationships sub-bucket. Recorded in the rules-engine + design (§10.1, resolved). - **R2b.** ~~Should `create_relationship_attribute/3` be fixed to honor kind sub-categories (13–16) instead of dropping new arc-labels - directly under nref 8?~~ **Resolved by L8 (2026-05-31):** + directly under nref 8?~~ **Resolved (attribute-placement + generalisation, 2026-05-31):** `create_relationship_attribute_pair/4` takes an explicit, validated `ParentNref`, so arc-labels can be filed under nref 13–16 (or any attribute parent); the `/3` arity keeps the nref-8 default. @@ -67,13 +68,14 @@ brainstorm but never answered. Most are smaller than (A)-vs-(B): Currently no Templates exist at end of bootstrap, so the diagram is clean. Decision pending first runtime Template seed. **Update 2026-06-01:** the `create_class/2` auto-default-template behavior is - reviewed and **kept** per-class (F4 design D14 — singleton/removal - both rejected). So once F4 Phase A lands, the two *instantiable* - meta-classes (`CompositionRule`, `ConnectionRule`) will each seed a - default template into the env tree; the abstract `Rule` root will not - (F4 D15 — non-instantiable classes skip the auto-default via the - deferred `instantiable => false` marker). Revisit the diagram then. -- **R4.** Promote the L7 sub-grouping pattern (sub-group per owning + reviewed and **kept** per-class (per the rules-engine design — + singleton/removal both rejected). So once the rules data model lands, + the two *instantiable* meta-classes (`CompositionRule`, + `ConnectionRule`) will each seed a default template into the env tree; + the abstract `Rule` root will not (non-instantiable classes skip the + auto-default via the `instantiable => false` marker). Revisit the + diagram then. +- **R4.** Promote the literals-subtree sub-grouping pattern (sub-group per owning worker, idempotent ensure-by-name under a category/attribute parent) to **policy** or keep it as **precedent only**? - **R5.** Where do the **shared creators** live? Module location, @@ -130,7 +132,7 @@ These are cheap and useful under both (A) and (B). 1. This file (re-orient). 2. `arcs-authoritative.md` (current arc model). -3. `docs/designs/f4-graphdb-rules-design.md` §10.1 (the P1 pinned - question — directly affected by R2). -4. `memory/project-f4-phase-a-pinned-question.md` (the current - blocker on F4 Phase A). +3. `docs/designs/f4-graphdb-rules-design.md` §10.1 (the pinned + placement question — directly affected by R2). +4. `memory/project-f4-phase-a-pinned-question.md` (the then-current + blocker on the rules data model). diff --git a/memory/project_m6_language_class_gap.md b/memory/project_m6_language_class_gap.md index 31da996..7e12b6b 100644 --- a/memory/project_m6_language_class_gap.md +++ b/memory/project_m6_language_class_gap.md @@ -1,6 +1,6 @@ --- name: project-m6-language-class-gap -description: M6 plan is missing seeding of the Language superclass hierarchy under Classes (nref 3) — ARCHITECTURE §10 specifies it but the plan doesn't implement it +description: M6 plan is missing seeding of the Language superclass hierarchy under Classes (nref 3) — Architecture §10 specifies it but the plan doesn't implement it metadata: type: project --- @@ -9,7 +9,7 @@ M6 plan gap identified before implementation began. Two items deferred: **Gap 1 — Language superclass hierarchy (blocks M6 plan completeness)** -`graphdb_language:init/1` in the plan does not seed a `Language` superclass node under Classes (nref 3). ARCHITECTURE §10 specifies: "The abstract concepts — 'Human Language', 'Dialect', 'Grammar Rule', 'Word', 'Token', 'Syntax Rule' — are class nodes in the ontology under a `Language` superclass seeded at runtime under `Classes` (nref 3)." +`graphdb_language:init/1` in the plan does not seed a `Language` superclass node under Classes (nref 3). Architecture §10 specifies: "The abstract concepts — 'Human Language', 'Dialect', 'Grammar Rule', 'Word', 'Token', 'Syntax Rule' — are class nodes in the ontology under a `Language` superclass seeded at runtime under `Classes` (nref 3)." Currently `lang_human` is a direct child of Classes (3) in bootstrap.terms — there is no `Language` superclass above it. The fix is either: - Option A: Update bootstrap.terms to add `Language` as a bootstrap node and make `lang_human` a subclass of it (clean but requires bootstrap change). @@ -19,7 +19,7 @@ Decision on which option was deferred before implementation. **Gap 2 — Connection arcs to subcategory nodes (nrefs 32–35) — deferred** -ARCHITECTURE §10: "Domain membership is recorded by a lateral connection arc from each language class node to the appropriate subcategory (e.g., English → Human Languages, nref 32)." These are template-scoped CONNECTION arcs (char 31). Template infrastructure is not yet implemented. Cannot land in M6. The bootstrap.terms comment on English (10000) confirms this is deferred. +Architecture §10: "Domain membership is recorded by a lateral connection arc from each language class node to the appropriate subcategory (e.g., English → Human Languages, nref 32)." These are template-scoped CONNECTION arcs (char 31). Template infrastructure is not yet implemented. Cannot land in M6. The bootstrap.terms comment on English (10000) confirms this is deferred. **Why:** Bootstrap.terms carries the note `(b) composition arc to Human Languages category (nref 32) — requires` (cut off, referring to template infrastructure). From 8d793e3db71ad6c68f1d09db001b9ddc55afad28 Mon Sep 17 00:00:00 2001 From: "David W. Thomas" Date: Thu, 11 Jun 2026 21:09:53 -0400 Subject: [PATCH 2/2] permit running parallel ctests script. --- .claude/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/settings.json b/.claude/settings.json index cb3e5dc..a0d4dae 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -71,7 +71,8 @@ }, "permissions": { "allow": [ - "Bash(Echo *)" + "Bash(./scripts/test-ct-parallel.sh:*)", + "Bash(scripts/test-ct-parallel.sh:*)" ] } }