Repository navigation
Declare and enforce children and parent rules per node type (closes #3447) - #3475
Conversation
Pragmatic Code Review: PR #3475Review Type: Initial Review (no prior reviews or comments on the PR) Recommendation: REQUEST CHANGES. One blocker: markdown import now fails outright on documents it used to accept. Everything else is an improvement or a nit. SummaryThe design is right and matches ADR-089: one declaration per type, one derived What I ran:
Requirements Check
FindingsCritical1. [Critical] Markdown import now fails on documents with content nested under an indented numbered list or code block
gives The same happens for a continuation paragraph under an indented numbered item, and for a deeper line after an indented fenced code block. This reaches Scope: the trigger is narrow. I ran Recommendation: have the importer ask the registry before a node becomes a parent candidate. Don't push a type whose Important2. [Important] Changing
The mirror case is a type that stops extending a type named in a ADR-089 §4 says tightening needs a check that no existing node violates it; its Consequences section also accepts "refused on their next move". Recommendation: when 3. [Important] A generic node update on a schema node installs structural rules with no validation Confirmed by running it: The trigger copies whatever the properties say, so this path skips the named-type existence check, the empty-list check, the tighten-only check and the existing-node check. It cannot relax a base rule, because enforcement unions the chain. It can install a rule that names a nonexistent type, or a The probe also shows Recommendation: validate the two keys where every schema-node write passes ( 4. [Important] The retype pre-check inside a transaction reads outside it
Verified by reading, not executed. It is reachable from playbook actions ( 5. [Important] An empty test was left behind #[test]
fn tool_node_does_not_have_children() {}It asserts nothing and reads as coverage. Delete it; the registry test already pins Suggestions6. [Suggestion] 7. [Suggestion] End-to-end leaf coverage omits 8. [Suggestion] Nitpicks
Outside this PR's diff
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address-review summaryPushed in Addressed
Documented, not extended2 Changing Skipped
Also changed since the review
Verification
Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: a critical finding was fixed with a logic change in the importer, two new guards were added on the update paths, and the frontend registry was reworked onto the generator after a merge. |
Pragmatic Code Review: PR #3475Review Type: Re-Review (reviewed head Recommendation: REQUEST CHANGES. Every finding from the first review is addressed. Two new Important findings remain, both confirmed by running them: the importer fix now stores nested list content out of reading order, and Previous Review Summary: 11 recommendations (1 Critical, 4 Important, 3 Suggestions, 3 nits). 10 addressed in code, 1 (finding 2) resolved by documenting the boundary, which I accept. 0 not addressed. 2 new Important findings, 1 new Suggestion. What I ran
Previous findings
The strict rule-shape check in The generated TypeScript agrees with Rust and SQL. Requirements Check
Code Review FindingsImportant1. [Important] The importer fix stores nested content before the list it belongs to A line indented under a leaf now attaches to the nearest entry on is stored as:
Recommendation: attach a line indented under a leaf to the leaf's own parent, so it follows the leaf as a sibling. For example, push the leaf's Documents with a top-level numbered list and indented detail are unaffected: that list was never on the stack. 2. [Important]
This predates the review fixes, and the first review missed it. It matters more now: the new guard makes The same merge leaves stale keys when a rule changes variant: Recommendation: make the schema-definition write authoritative for the keys it owns. Remove Suggestion3. [Suggestion] A generic create of a schema node is checked for shape only Confirmed: Nitpicks
|
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address-review summary (re-review)Pushed in Addressed
Not changed
The test fixture for finding 1 no longer includes an indented code block: where an indented code block is parented (under the last paragraph) is existing importer behaviour this PR does not touch. Verification
Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: both fixes change logic (importer parenting, and how a schema definition is written to the row), so they get one more narrow look. |
Pragmatic Code Review: PR #3475Review Type: Re-Review 2 (reviewed head Recommendation: REQUEST CHANGES, for one small item. The Previous Review Summary: 5 recommendations (2 Important, 1 Suggestion, 2 nits). Important 2 ( What I ran
Previous findings
Importer: what holds
One thing I checked and am not raising: a bullet that follows a nested numbered list at the list's own indent ( Schema definition write: what holdsProbe on a user schema created abstract, with
Requirements Check
Code Review FindingsImportant1. [Important] A single-file import still stores nested content ahead of the list it belongs to The new branch pushes the leaf's parent only when there is one ( Confirmed at the parser, with The same input with a root id gives the order the new test asserts. A table and a code block under a bullet behave the same way ( Recommendation: pass the root id at that call, as every other caller does: Suggestion2. [Suggestion]
Nitpicks
CLAUDE.mdNothing in |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review 2): REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
506df5e to
a8d3338
Compare
Address-review summary (re-review 2)The branch is now one commit on current Addressed
The nit that the parser test covers the root-id path only is answered by the daemon test: no production caller passes Also changed
Branch collapsedThe merge queue replays a PR's non-merge commits onto VerificationOn the collapsed commit: Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: the one Important finding is fixed with the change the review specified (one argument at one call site), proven by a test at that entry point. The rest is a guard that skips no-op statements, a deleted uncalled function and a comment. No design or logic beyond that changed since the reviewed head. |
|
Merge queue: #3475 was taken out of the queue — it conflicts with main or with #3472, queued ahead of it, in: packages/core/src/behaviors/mod.rs, packages/core/src/models/core_schemas.rs, packages/desktop-app/src/lib/plugins/core-plugins.ts, packages/desktop-app/src/lib/types/generated/core-node-types.ts, packages/desktop-app/src/lib/types/generated/core-type-entry.ts, packages/desktop-app/src/tests/plugins/core-plugins.test.ts, packages/nodespace-types/src/bin/gen_ts.rs, packages/nodespace-types/src/core_type.rs. Rebase onto origin/main, push, and re-run |
Rebased onto main after the merge queue ejected this PRThe queue took #3475 out because it conflicted with #3472 (the ai-chat subtype split), queued ahead of it. #3472 has landed; this branch is rebased onto it and is still one commit. What the rebase changed, beyond resolving the conflicts:
The Verification on the rebased commit: No design change since the last review round, so no further re-review; re-queueing. |
a8d3338 to
07f3b20
Compare
…3447) Implements ADR-089. Each type declares which children its nodes may have and where they may sit, and the database enforces both on every write path. Declaration - The registry carries the rules: collection, schema and date are must_be_root; code-block, ordered-list, horizontal-line, table, query, tool and database-settings take no children; everything else, ai-chat included, is any / any. - SchemaNode gains children and parent. Seeded core schemas declare the registry's rules; create_schema and update_schema accept them for user-defined types. A subtype only tightens: a declaration that relaxes its base, or that an existing node already breaks, is refused. A core type's rules cannot be changed. update_schema is the one way to change a rule, and can loosen one back to any. - The rule types and each core type's rules are generated into TypeScript. Enforcement - A derived structural_rule table (core rows from the registry, the rest written by triggers in the statement that writes the schema) and three generic triggers replace the per-type root triggers. They join the type ancestry, so a rule on a base type binds its subtypes. - The store asks the same SQL before it writes, so create, move, bulk import, generic relationship create and delete, merge and retype return a TreeInvariantViolation naming the rule and the nodes: must_be_root, children_none, child_not_allowed, parent_required. - A type that needs a parent is never created or left without one; that check has no trigger behind it and reads on the caller's transaction. Replaced - NodeBehavior::can_have_children() and NodeServiceError::NotAContainer. Embedding aggregation asks the children rule and stops at a chat, whose children are their own embedding roots. - The frontend canHaveChildren / canBeChild plugin flags. Indent, outdent and type conversion ask the rules first. - Markdown import places lines indented below a type that takes no children after it, as its siblings, on every import path. The ai-chat-message rule is not declared: that type is not in the registry yet. must_have_parent_of is implemented and tested through user-defined types. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
07f3b20 to
f20fbcf
Compare
Closes #3447. Implements ADR-089: each type declares which children its nodes may have and where they may sit, and the database enforces both rules on every write path.
What changed
Declaration
CoreNodeType) carries the rules.collection,schemaanddatearemust_be_root;code-block,ordered-list,horizontal-line,table,query,toolanddatabase-settingsarechildren: none; everything else,ai-chatincluded, isany/any.SchemaNodegainschildrenandparent({"rule": "any_except", "types": [...]},{"rule": "must_have_parent_of", "types": [...]};anyis the default and is not stored). Seeded core schemas declare the registry's rules.create_schema/update_schemaaccept them for user-defined types.Enforcement
structural_rule: core rows seeded from the registry, every other type's rows written by triggers in the statement that writes its schema.structure_has_child_insert,structure_has_child_update,structure_node_retype) replace the per-type*_is_root_*triggers. They jointype_ancestry, so a rule on a base type binds its subtypes and a named type covers its own.TreeInvariantViolationnaming the rule and the nodes. Rule names on the wire are nowmust_be_root,children_none,child_not_allowed,parent_required(replacingcollection_not_root/schema_not_root);NodeServiceError::NotAContaineris gone.must_have_parent_ofnode is never created or left without a parent: creating it as a root (single and bulk), moving it to the root and removing its parent edge are refused before anything is written. No trigger can see a missing edge, so this check is Rust-only.Replaced
NodeBehavior::can_have_children()is removed. Embedding aggregation asks thechildrenrule and stops at a chat.canHaveChildren/canBeChildplugin flags andpluginRegistry.canHaveChildren()are removed. The rule types and each core type's rules are generated from Rust (bun run gen:types);core-node-types.tscomposes them down theextendschain and resolves user-defined rules from the loaded schemas. Indent, outdent and type conversion askcanHaveChild/canBeRootfirst.Not in this PR
ai-chat-message's rule is not declared. That type andai-chat-native/ai-chat-acpare not in the registry yet (Split ai-chat into an abstract base with ai-chat-native and ai-chat-pty subtypes #3446, Store AI chat messages as ai-chat-message child nodes with real edges #3448).must_have_parent_ofis implemented and tested through user-defined types; declaring it on the message type is a one-line registry entry in the PR that adds the type.Behaviour changes worth knowing
children/parentchange only throughupdate_schema; a generic update that changes them is refused.datenode under a parent is now refused asmust_be_rootrather than as a hierarchy violation.Tests
structural_rules_test.rs(15 end-to-end tests): declaration, subtype tightening, tightening against existing nodes, root-only types and their subtypes, leaf types on create / move / relationship, bulk import, generic relationship, retype, the needs-a-parent paths, chat children as embedding roots.bun run test:changedpasses (frontend, scripts, browser, Rust);bun run quality:fixclean.Node type sequence
Kind and category of every type are unchanged; this adds structural rules only. Docs updated in nodespace-docs (
components/node-types.mdstructure lines,components/schema-management.mddeclaration syntax and enforcement).🤖 Generated with Claude Code