Repository navigation
Validate other builds' fields on core relationships from build_shared_services - #3626
Conversation
Pragmatic Code Review: PR #3626 (medium depth)Review Type: Initial Review. The PR had no reviews or comments yet.
Requirements CheckSources: #3414 "Typed subtypes and edge fields", ADR-082 §2.2/§5/§8/§9, and ADR-083 §2.
Write-path coverage (checked independently)The PR body says the inventory "is in the review thread", but the PR has no thread yet. Here it is.
The direct Validating before the idempotency no-op is right. Without it, a create of an existing edge with an invalid bucket would report success for data that was neither valid nor stored. Refusing it is fail-fast input validation. It also makes the two Limiting declarations to
FindingsNo 🔴 Critical or 🟡 Important findings. None of the Critical-checklist items apply:
🟢 Suggestion: make the versioned surface cheap to widen
🟢 Suggestion: enum declarations are checked less strictly than schema
🟢 Suggestion: the
🟢 Nit: comments suggest declared relationships can carry a registered bucket
🟢 Nit: the doc list misses one refusal
What's good
Recommendation: APPROVEThis is a clear net improvement. It meets the section's acceptance criteria, ADR-082 §2.2/§5/§8/§9 and ADR-083 §2. The suggestions are optional polish. Only the |
mstomar125
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Address review (round 1)I addressed every 🟢 finding of the APPROVE review in ✅ Addressed
Mutation check of the fixes. All 3 mutants are killed:
Local runs:
Coordination. Michael's open #3630 shares four files with this stack but none of the functions I changed. A merge-tree of the stack with #3630, and of the stack onto current main, are both clean. Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: These are small, low-risk changes: a const type, an un-exported alias, one more declaration check with its test, a new unit test, and comment and doc wording. Each is mutation-checked. 🤖 Generated with Claude Code |
28c798b to
5a3cc88
Compare
5a3cc88 to
31ec8f2
Compare
0b163cf to
48ab70c
Compare
|
Rebased onto main after #3625 merged: 🤖 Generated with Claude Code |
…_services DaemonExtensions::edge_fields(declaration) adds typed fields to a core relationship (ADR-082 §2.2). They are stored in the declaring extension's bucket of the edge's properties, properties.<extension id>, and every database the daemon opens validates that bucket on each edge write through its node API. - nodespace_core::extensions::EdgeFieldDeclaration: a relationship, the extension id, scalar fields (text, number, boolean, date, datetime, enum with its values; optionally required) and an optional validator of the whole bucket. DataExtensions::edge_fields carries them; NodeService::new_with_extensions builds the node service's registry. - The node service checks the registered buckets in create_relationship (built-in branch), write_relationship_in_tx (declared relationships, Play actions, multi-edge creates) and update_relationship_properties, before anything is stored. A present bucket must be an object of declared fields, each null or of its type, with every required field set; then the validator runs. A bucket nobody registered is never read. - Rejected at startup: a relationship other than member_of and has_role (core recreates has_child edges on a move and mentions with the content, which would drop a bucket), an extension id that is not one or is `order`, a second declaration for one relationship and id, and a malformed field. - NodeService::scalar_mismatch and describe_received are crate-visible so edge fields are type-checked as node fields are. - Fixtures: an edge field on member_of in core (stored in its bucket on both member_of create paths; refused on create, in a transaction, on update and on an existing edge; other buckets left alone) and in the daemon (validated in every database; a has_child declaration stops startup). - EXTENSION_API_VERSION 2.6: a new daemon hook (minor). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… type - EDGE_FIELD_RELATIONSHIPS is a slice, so adding a relationship later is a minor change rather than a new array type. - The validator's Arc alias is internal; with_validator takes any closure. - An enum edge field listing a value twice is refused, as an enum edge field in a schema declaration is. - An unnamed field is reported as such, and the edge_fields doc lists every refusal. - A unit test checks each scalar type admits its values, refuses another JSON type and accepts null. - The update path's comment says only a built-in relationship can carry registered fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48ab70c to
0de9868
Compare
Part of #3414 (the "Typed subtypes and edge fields" section, ADR-082 §2.2 and §5). This is the third of three PRs, and the last open section of that umbrella. It is stacked on #3625: the base is
issue-3414-typed-subtypes, so the diff shows only this PR's commit. Retarget it tomainonce #3624 and #3625 land.What changes
DaemonExtensions::edge_fields(declaration)adds typed fields to a core relationship.properties.<extension id>.*(for exampleproperties.fixture.permissiononmember_of), beside core'sorder.Core (
nodespace_core::extensions):EdgeFieldDeclaration::new(relationship, extension_id, fields), with an optional.with_validator(|bucket| …)over the whole bucket for cross-field rules.EdgeFields with a scalar type (text,number,boolean,date,datetime, orenumwith its values), optionallyrequired.DataExtensions::edge_fieldscarries them, andNodeService::new_with_extensionsbuilds the node service's registry.Where the check runs. The node service checks registered buckets at the three places a caller's edge properties enter, before anything is stored:
create_relationship, built-in branch, which covers bothmember_ofpaths, auto-ordered included;write_relationship_in_tx, which covers declared relationships, Playadd_relationshipactions and multi-edge creates;update_relationship_properties, for built-in and declared relationships.An idempotent create of an existing edge is checked too. Every other edge write in core builds its own properties (
{}or{"order"}); the write-path inventory is in the review thread.What a bucket must satisfy. It must be an object of declared fields, each null or of its type (dates checked as node fields are, through
NodeService::scalar_mismatch), with every required field set. Then the validator runs.Unregistered buckets. A bucket nobody registered is never read. That includes the build's own bucket on a relationship it declared nothing for.
Rejected at startup (through
DataExtensions::check):member_ofandhas_role(EDGE_FIELD_RELATIONSHIPS). Core never rewrites the properties of those two. It recreateshas_childedges on a cross-parent move ormove_children_to_parent, andmentionsedges follow the content, so a bucket on either would be silently dropped.order, the key core stores on its edges.default(core never writes the bucket),indexedortargetType.Daemon:
DaemonExtensions::edge_fieldsdelegates to the core value.DaemonExtensionsError::Data(non-exhaustive since #3624) carriesDataExtensionsError::EdgeFields { relationship, extension_id, reason }.With no declaration, the registry is empty and
validatereturns at once. Every edge write behaves as before, and undeclared keys on an edge stay a supported, untouched shape.Acceptance mapping (#3414, typed subtypes and edge fields)
build_shared_servicestakes edge-field declarations per relationship type (extension id, typed fields, optional validator). Extension edge fields are stored and validated in the extension's bucket (properties.<id>.*), and core never reads an unregistered bucket.member_of.With #3624 and #3625, every item of the section is covered.
Tests
Core fixture (
packages/core/src/extensions/fixture_tests.rs, watched since #3625). The fixture declarespermission(a required enum:admin,modify,read_only) andsince(a date) onmember_ofin thefixturebucket, with a validator that makesadminneedsince.an_edge_field_on_member_of_is_stored_in_the_extension_bucket: both create paths store the bucket besideorder.an_invalid_bucket_is_refused_on_create: six ways a bucket can be wrong, each refused with nothing stored:an_invalid_bucket_is_refused_in_a_transaction:create_relationship_in_tx.an_invalid_bucket_is_refused_on_update_and_on_an_existing_edge.a_bucket_nobody_registered_is_left_alone: three cases:member_of;mentions;a_node_service_with_a_refused_edge_field_declaration_is_refused: the refusal comes before anything is written.Unit tests:
extensions::tests::an_edge_field_declaration_on_member_of_or_has_role_is_accepted: every scalar type on both relationships.…a_malformed_edge_field_declaration_is_rejected: 13 malformed declarations, the duplicate, and acceptance across relationships and ids.Daemon fixture (
extension_points_fixture.rs, throughbuild_shared_serviceswith the isolated home and no model):an_edge_field_is_validated_in_every_database: two databases each refuse a bad value and store a good one in the bucket.an_edge_field_declaration_on_has_child_stops_startup.Mutation check. 19 mutants, all killed. Every new test is killed by at least one:
create_relationshipbuilt-in branch skips the check…refused_on_create,…on_update_and_on_an_existing_edgewrite_relationship_in_txskips it…refused_in_a_transactionupdate_relationship_propertiesskips it…on_update_and_on_an_existing_edgea_node_service_with_a_refused_edge_field_declaration_is_refuseda_bucket_nobody_registered_is_left_alone(thementionscase)a_bucket_nobody_registered_is_left_alone…refused_on_create…refused_on_create…refused_on_create…refused_on_createa_malformed_edge_field_declaration_is_rejected,a_node_service_with_a_refused_…orderaccepted as an ida_malformed_…a_malformed_…datetimenot an edge typean_edge_field_declaration_on_member_of_or_has_role_is_accepteda_malformed_…DaemonExtensions::edge_fieldsdrops itadd_to_collectiondrops caller data (existing code)an_edge_field_on_member_of_is_stored_in_the_extension_bucketLocal runs (crate-scoped, under the machine slot):
cargo clippy -p nodespace-core -p nodespace-daemon --all-targets -- -D warnings -D clippy::unused-async: clean.--lib extensions:: relationship member_of collection edge abstract unknown retype instantiable: 378/378, which includes core's existing relationship write-path tests.extension_points_fixture required_extensions services::extensions per_db: 26/26.bun test scripts/: 1441/1441;Extension API
2.5 → 2.6 (minor). This adds a daemon hook:
DaemonExtensions::edge_fields,EdgeFieldDeclarationandDataExtensions::edge_fields.version).Documentation
NodeSpaceAI/nodespace-docs#109 (branch
issue-3414-typed-subtypes) gains:"Not built yet" becomes "Outside this guide" (the §7 deferred points and the §5 daemon points with their own APIs), because every hook of this section is now built.
🤖 Generated with Claude Code