Repository navigation
Closed
Labels
Description
Overview
A schema edit can declare an extension field whose bare name is already a declared field of the type: custom:status on task, beside status. A rule or a query reads an extension field by its bare name, and the declared field always wins that name, so the new field can be stored and written but never read. Refuse the name when the schema is edited, as a schema is already refused a core type's name.
Problem Statement
create_schema,update_schema(add_fields,rename_fields) and theextendsre-target all accept a field namedcustom:/org:/plugin:+ the name of a field declared in the type's chain.- The field is unreadable:
node.statusin a Play condition and astatusquery filter resolve to the declared field. Nothing tells the user. - The node write path refuses an undeclared
custom:statuson such a node, but has to let a schema-declared one through so that a declared field with a default does not fail every create. The two validators disagree about which names are legal.
Proposed Solution
- One check, run at every schema-edit entry point over the fields the edit would leave on the schema: a field whose name is an extension prefix +
Xis refused whenXis declared as an unprefixed field anywhere in the type's chain (own fields, the fields in the same call, and every ancestor's). - The error names the declared field and the schema that declares it, in the shape of the write-side refusal ("'custom:status' cannot be declared: 'status' is a declared field of the type 'task' ...").
- Remove the write path's exemption for a schema-declared key, and the test that pins it: once no schema can declare such a field, the exemption has nothing to protect.
- No migration: a database whose schema already holds such a field is reset.
Documentation
- ADR-097 §6 - the rule: a write and a schema edit both refuse an extension field named after a declared field, checked against the whole chain
- ADR-063 - where a prefix is required (extending a core type) and where it is not (a user-defined type)
- ADR-078 - composition is additive only; the existing redeclaration check this one sits beside
- ADR-086 - closed core schemas and the registry
- Updates: the extension-field sentence in the
tasksheet's Typed update bullet incomponents/node-types.md(it describes the write-side refusal only)
Architecture Impact
Extends ADR-097 §6, which is already amended and pushed to say a schema edit is refused the same way as a write. No decision is reversed. No node type, field, relationship or structural rule is added or changed, so the node type sequence does not apply.
Acceptance Criteria
-
update_schemaadd_fieldswithcustom:statusontaskis refused, and the error namesstatusandtask - The same for
org:andplugin:prefixes, and for a name declared by an ancestor (a user subtype oftaskaddingcustom:status) -
rename_fieldsto such a name is refused -
create_schemais refused when its own fields hold bothXandcustom:X, and when itextendsa type that declaresX - Giving a schema a new parent is refused when one of its prefixed fields is named after a field the new chain declares
- A prefixed name that matches no declared field is still accepted (
custom:effortontask) -
org:custom:xbeside a declaredcustom:xis not refused: it reads ascustom:x, which the declared field (read asx) does not shadow - The schema is unchanged after each refusal
- The write path no longer exempts a schema-declared key, and its test is removed
- The friendly-name disambiguation tests no longer rely on
custom:statusbesidestatus - Each new test fails with the check removed
- Code passes
bun run quality:fix -
bun run test:changedpasses
Technical Specifications
Reference Files
packages/core/src/schema/mod.rs-handle_create_schema,handle_update_schema, andvalidate_no_field_redeclaration, which already resolves the chain's field owners (resolve_field_owners) and is called from all three paths: creation,add_fields, and the re-target. The core-type-name refusal ("is a core type; a schema cannot take its name") is the precedent for the messagepackages/core/src/services/node_service/crud.rs-reject_shadowed_extension_fields: the write-side refusal, and theowners.contains_key(key)exemption to removepackages/core/src/services/node_service/mod.rs-extension_field_bare_name: the one definition of the prefixes and of the name a field is read by. Reuse itpackages/core/src/services/node_service/schema.rs-rename_schema_field, which carries its own inherited-field checkpackages/core/src/playbook/cel.rs(node_to_cel_value_at_scope) andgraph_resolver.rs- why the field is unreadable: unprefixed keys are taken across the whole chain before any prefixed onepackages/core/src/schema/schema_test.rs-test_update_schema_add_fields_disambiguates_friendly_name_colliding_with_existing_field,test_update_schema_add_fields_does_not_rewrite_an_explicit_colliding_friendly_nameandtest_rename_field_friendly_name_empty_string_disambiguates_against_a_sibling_fieldbuild their label collision fromcustom:status/custom:due_datebeside the bare name. Give them another pair of fields that derive the same labelpackages/core/tests/it/core_type_registry_test.rs-an_extension_field_named_after_a_declared_field_is_rejected,a_subtype_refuses_an_extension_field_named_after_any_field_in_its_chain(the write-side tests), andan_extension_field_the_schema_declares_is_still_written(to remove)
Implementation Details
validate_no_field_redeclarationonly runs when the schema has a parent. A schema with none (taskitself, or a user-defined type) needs the check against its own fields, so the check cannot live only there.- Compare with unprefixed declared names only: a bare name that is itself prefixed shadows nothing.
- The error text reaches the agent as a tool result. Say what to do next: use the declared field, or pick another name.
Related Issues
- Remove requires_spec from task; the spec rule applies only to a task that links a spec #3718 (added the write-side refusal and left this path out)
Non-Goals
- Two extension fields that read as the same name under different prefixes (
org:estimatebesidecustom:estimate) - An extension field named after a relationship (
custom:specon a task) - A prefixed key stored beside a node's own bucket
Activity
malibio commentedon Oct 11, 2026
Frontend baseline: 324 test files passed, 5873 tests passed (shared worktree with #3728)
Refuse a schema field named after a declared field behind a prefix (c…