Repository navigation
Delete unused node models, plugins and compatibility shims - #3469
Conversation
Pragmatic Code Review — PR #3469
SummaryA 2,796-line deletion that does what the issue asks, and both deviations from the issue text are correct on the evidence. The Rust side is clean. The one real problem is in the frontend: swapping Requirements Check
The two deviationsPlay rules (item 7) — correct.
Deleting the Query field lookup (item 6) — correct in approach, one regression. Reading through Code Review Findings🟡 Important —
|
Filter (type: 'metadata') |
Result on this branch |
|---|---|
node_type equals 'invoice' on an invoice node |
false |
created_at gte '2026-01-01' on a node created 2026-10-01 |
false |
title contains 'hell' on title hello |
false |
content contains 'hell' |
false |
On main the removed readFieldValue resolved these through the camelCase top-level key (node_type → nodeType, created_at → createdAt) or the same-named key (title, content); that is from reading the removed code, not from a run on main.
Effect: shouldShowCreatedNode returns false for any saved query carrying a metadata filter, so a node created by the CLI, the agent or another tab never live-appends to that view until the next query load. It fails closed, the same degradation parent/children filters already have, which is why this is not a blocker on severity alone. But the filters are reachable: the agent tool schema offers metadata for exactly these fields (agent/src/local_agent/tools.rs:658) and methodology install handles metadata/node_type filters on views (methodology/install.rs:604). The doc comment at :175 still says metadata filters are supported "fully", and no test covers them, which is how this slipped through.
Fix: give metadata its own branch with an explicit map mirroring the backend allowlist — created_at → node.createdAt, modified_at → node.modifiedAt, node_type → node.nodeType, content → node.content, title → node.title — and add a test per field. That is a declared mapping, not a spelling guess, so it stays within the intent of this PR. Principle: a shim removal must not change behaviour for inputs the shim was legitimately serving; the documented contract and the implementation must agree.
🟡 Important — collection icon / color still read
📁 packages/desktop-app/src/lib/plugins/core-plugins.ts:541-542
collectionNodePlugin.extractMetadata still returns icon: properties.icon and color: properties.color. The acceptance criterion says no remaining reference to collection icon/color, and the type no longer declares them. Both lines are also no-ops, since ...properties follows. Delete them (and consider whether the whole extractMetadata is needed: it reduces to { description, ...properties }, which is properties). Principle: YAGNI, and the issue's own "no remaining reference" criterion.
🟢 Suggestion — user / document type remnants outside the plugin file
📁 packages/desktop-app/src/lib/design/icons/registry.ts:92,130, packages/desktop-app/src/lib/design/components/base-node.svelte:158,179, packages/desktop-app/src/app.css:665,667
The plugin definitions are gone, but the icon registry still registers document and user icons, base-node.svelte carries mock autocomplete entries with those node types, and --node-document / --node-user tokens remain. The issue scopes item 3 to the plugin definitions, so this is not a criterion miss, but it is the same dead code by the same reasoning ("no backend type exists for either"). Worth folding in here rather than leaving it for later. Check noderef.css:304 first: --node-user is also used for a reference style there, so that token may have to stay.
🟢 Suggestion — test the regression class, not just the happy path
📁 packages/desktop-app/src/tests/components/query-node-model.test.ts
The new tests cover custom fields and typed core fields. Add metadata cases (with the fix above) and one case for a subtype row (a type that extends task, whose inherited fields arrive flat in properties because promotion is keyed on the exact type). The subtype case works today by my reading, but it is the next place this lookup will be wrong if TYPED_CORE_FIELDS and promoted_fields drift.
Nitpicks
- Nit: 📁
packages/core/src/playbook/validation.rs:2080-2082— the comment now wraps awkwardly ("no node_type/relationship_type to / reference a schema through"); reflow it. - Nit: 📁
packages/desktop-app/src/lib/types/typed-core-fields.ts:89—typedCoreFieldmatches the wire name as well as the storage name, so a column or filter nameddueDatestill resolves on a task. Pre-existing and not changed here, but the PR description's "no spelling is guessed" is slightly stronger than the code.
Out of scope, noted for the record
packages/nodespace-types/src/convert.rs:215-219, 264-279 still reads the "legacy dueDate" spelling from stored properties. That is compatibility code for an older shape, but it belongs to the task-defined-once work the issue lists as a non-goal, so it does not count against this PR.
What I verified and what I did not
- Verified by grep across
packages/(excludingtarget/,node_modules/): every "no remaining reference" claim in the table above. - Verified by reading: every production caller of
parse_rules_from_propertiesandactivate_play; column and filter production for the query table; the three deleted-test groups. - Ran:
table-row.test.tsandquery-node-model.test.ts(42 passed); a probe script for the metadata filter cases. - Not run: the Rust suites,
test:changed,quality:fix(review constraints).
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.
Review feedback addressedAddressed
Skipped
Verification: frontend unit suite 299 files / 5526 tests passed; Re-Review DecisionDecision: RE-REVIEW NEEDED Rationale: the metadata-filter fix adds new logic to filter evaluation rather than only deleting code. |
Pragmatic Code Review — PR #3469 (Re-Review)
Previous Review Summary
Both Important findings are fixed correctly. The new commit touches four files and stays within the review's scope. Status of each previous finding
What I checkedRemoving The new tests pin the fix and fail on the pre-fix code. I ran the file: 43 passed. By reading the pre-fix lookup ( Frontend evaluation against the backend's SQL. I probed the edge cases on this branch:
The first six rows are fine for a function whose job is deciding whether to live-append a node. The last two fail open, but both come from Code Review Findings🟢 Suggestion —
|
property |
exists result |
|---|---|
constructor |
true |
toString |
true |
hasOwnProperty |
true |
__proto__ |
throws TypeError: … is not a function |
The doc comment says a name outside the list "has no value", and that is not true for these names. Reachability is low: the backend rejects any metadata field outside its allow-list, so a query carrying one errors instead of settling, and shouldShowCreatedNode only evaluates filters when queryState === 'success'. I did not trace whether every path into the gate guarantees that, so I would close it rather than rely on it.
Fix: guard with Object.hasOwn(METADATA_FIELDS, filter.property) or make METADATA_FIELDS a Map. Add constructor to the "unknown column" test, which today only covers status. Principle: an allow-list must reject everything not listed; the documented contract and the implementation should agree.
🟢 Suggestion (pre-existing, author's call) — case-sensitivity default disagrees with the backend
📁 packages/desktop-app/src/lib/components/query/query-node-model.ts:209; packages/core/src/services/query_service/mod.rs:803,810
The frontend defaults caseSensitive to false for equals, in and contains. The backend defaults contains to case-sensitive (unwrap_or(true)), and equals / in are a plain = / IN that ignore the flag. So with no flag set, the frontend matches where the backend does not, and a node can live-append to a view whose query excludes it until the next load. That is the direction the matchesFilter doc comment says to avoid.
This is not introduced here and affects property and content filters equally. I raise it because the new test matches title and content (value: 'acme inv' against title Acme invoice, no caseSensitive) now pins the divergent default for metadata filters. Either align the default in this PR, or change the fixture to a same-case value so the test does not assert behaviour the backend contradicts. Not blocking.
Nitpick
- Nit: 📁
packages/desktop-app/src/lib/components/query/query-node-model.ts:182—title: (node) => node.titlehas no trailing comma while the rest of the file uses them.quality:fixreportedly passed, so the formatter accepts it; cosmetic only.
What I verified and what I did not
- Ran:
query-node-model.test.ts(43 passed); a scratch probe of the metadata edge cases above (outside the repo; the worktree is clean). - Verified by reading: the backend allow-list and
build_filter_condition; timestamp storage (to_rfc3339intoTEXTcolumns); everyextractMetadataconsumer; the full diff of the commit. - Not run: the pre-fix commit against the new tests (failure on the old code is from reading it plus the initial review's probe); Rust suites;
test:changed;quality:fix.
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
Re-review feedback addressedAddressed
Skipped
Verification: Re-Review DecisionDecision: NO RE-REVIEW NEEDED Rationale: the re-review approved; this commit is a one-line own-key guard plus test fixture changes, with no new logic paths. |
) - Remove NodeBehavior::default_metadata and every implementation - Remove the TextNode, DateNode, CodeBlockNode, QuoteBlockNode, OrderedListNode and CollectionNode wrappers from core - Remove the user and document plugin definitions - Remove embeddingVector and memberOf from the TypeScript Node, and icon and color from CollectionNode - Query table and filter evaluation read a field through resolveFieldValue (typed key for a core type's declared field, stored name in properties otherwise) instead of guessing a camelCase spelling - parse_rules_from_properties reads only the stored location, properties.play.rules; in-memory test fixtures use the stored shape - Remove the target_type action parameter check from play validation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- matchesFilter evaluates a metadata filter against the node column it names (created_at, modified_at, node_type, content, title), mirroring the backend allow-list, instead of looking for a schema field - Remove the collection plugin's extractMetadata, which only restated properties and still named icon and color - Tests for each metadata column and for a subtype's inherited field - Reflow a comment in play validation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A metadata filter naming an inherited object key (constructor, __proto__) has no value, like any other name outside the allow-list - The title and content cases compare same-case values, so they do not pin a case-insensitive default Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dc4fdd9 to
00db636
Compare
Closes #3430
What changed
Deletes code that production never uses and the compatibility shims that read keys storage never holds.
Unused Rust (core)
NodeBehavior::default_metadata()and every implementation, with the tests that only exercised it.TextNode,DateNode,CodeBlockNode,QuoteBlockNode,OrderedListNodeandCollectionNodewrappers and their test files. Nothing outside their own files referenced them.Unused frontend
useranddocumentplugin definitions (never registered; no backend type).embeddingVectorandmemberOfon the TypeScriptNode(alsoembeddingVectoronNodeUpdateandNodeWithChildren; the backend has no such field).iconandcoloron the TypeScriptCollectionNode.Compatibility shims
toCamelCase()inquery-node-model.tsandtable-row.svelte. Both now read a field through the existingresolveFieldValue: a core type's declared field comes from its typed key (due_date→dueDate, decided byTYPED_CORE_FIELDS), and every other field comes frompropertiesunder its stored snake_case name. No spelling is guessed.parse_rules_from_properties.target_typeaction parameter check in play validation.Two places this differs from the issue text
properties.play.rulesthe fallback. It is the stored location: a play's declared fields are bucketed under its type on write,validate_play_rulesruns after bucketing, andnode-types.mddocumentsrulesatplay.rules. The read that storage never satisfies is the top-levelproperties.rules, so that is the one removed. Deleting theplay.rulesread instead would fail every persisted play withMissingField("rules"). In-memory test fixtures that activate a play directly were built in the top-level shape; they now use the stored shape (plays created throughcreate_nodestill pass flat input, which the service buckets).due_date/first_name/start_datecolumns on task, person and project, whose declared fields travel as typed camelCase keys. The lookup now goes through the declared typed-field registry rather than a name transformation, so custom fields are read by stored name only and core fields by their declared key.target_typeremains in the codebase as the schema relationship field and the query field; only the play action parameter check is removed.Docs
No change needed in nodespace-docs:
components/node-types.mdhas noicon/colorrows for Collection and already documents rules atplay.rules; no doc describes the removed wrappers ordefault_metadataother than ADR-086, which records the decision to delete them.Testing
bun run test:changedpasses (frontend, scripts, browser, Rust workspace).bun run quality:fixclean.properties, ignore a camelCase spelling, and render a core type's declared field from its typed key; filter evaluation covers the same three cases;parse_rules_from_propertiesrejects rules outside theplaybucket.🤖 Generated with Claude Code