Repository navigation
isCustomSchemaType conflates "is core" with "has a dedicated frontend integration" across 4 call sites #1998
Description
Overview
isCustomSchemaType(nodeType) (packages/desktop-app/src/lib/design/components/node-type-predicates.ts:31-33) is defined as !CORE_NODE_TYPES.has(nodeType) — it answers "is this type absent from a hardcoded core-type list," and every call site actually uses it to answer a different question: "does this type have a dedicated frontend integration (hardcoded form, viewer, open-button behavior, display-name plugin) or does it need the generic schema-driven fallback." Those two questions coincided for every core type until project was added — deliberately, per its own commit message — as a core type with no frontend integration at all. The predicate is now stale: it encodes an assumption ("core type" implies "has a bespoke frontend integration") that is no longer true, and every one of its 4 real call sites inherits the same wrong behavior for project (and will for any future type in the same situation).
Problem Statement
Origin of the stale assumption: 35db68b9 ("Register project as a built-in frontend node type (closes #1754)") added 'project' to CORE_NODE_TYPES specifically to fix sidebar/icon classification, and says so explicitly: "Scoped to nav/icon — no viewer or behavior mirror." That commit knowingly created a type that is core-classified but has no frontend form/viewer — the exact combination isCustomSchemaType was never designed to represent, since previously every type was cleanly one or the other.
Every real call site breaks the same way for project today:
-
packages/desktop-app/src/lib/design/components/base-node-viewer.svelte:1348— gates whether the generic schema-driven properties form renders at all.projectgets neither the hardcoded form (none registered) nor the generic one (blocked by this check) → no properties UI whatsoever. (Originally filed as this issue's initial scope.) -
packages/desktop-app/src/lib/design/components/schema-form-loader.svelte.ts:62— gates whetherloadGenericSchema()is even called:if (isCustomSchemaType(nodeType)) { this.loadGenericSchema(nodeType); }. This runs before branch 2 inbase-node-viewer.svelteever gets a schema to check — so fixing only the render-gate inbase-node-viewer.svelteis not sufficient;genericSchemawould never be populated forprojectin the first place. -
packages/desktop-app/src/lib/design/components/node-row.svelte:164— gates the inline "open in other pane" button shown on a custom-entity child row:{#if isCustomSchemaType(node.nodeType)}. Aprojectnode rendered as a child row gets no open-button affordance. -
packages/desktop-app/src/lib/services/navigation-service.ts:140, 172— gates (a) using the plugin's registered display name for the tab/header title, and (b) whether clicking aprojectnode navigates directly to it vs. to its parent. Both fall through to non-custom-entity behavior forproject, which may or may not be appropriate — needs verification per site.
Proposed Solution
Replace the "is this NOT in the core list" test with a test for what each call site actually needs: whether a type has a hardcoded frontend integration already claiming it, vs. whether it needs the generic schema-driven path. Concretely, this likely means:
- Determine "has a dedicated form/viewer" from the plugin registry itself (e.g.
pluginRegistry.getPlugin(nodeType)returning a registered form/viewer) rather than from list membership — this is self-updating as new types are registered, unlike a hand-maintainedSet. schema-form-loader.svelte.ts'sloadGenericSchemagate (call site 2) should probably become close to unconditional (or gated on "no hardcoded form was found for this type," which the surrounding code in that file already determines in the branch just above it) — load the generic schema whenever there isn't a more specific one, independent of core/custom classification.- Audit each of the 4 call sites individually rather than assuming one shared replacement predicate fits all of them —
node-row.svelte's open-button andnavigation-service.ts's routing/naming behavior may have legitimately different correct conditions than the form-rendering gate. Confirm intended behavior for each before changing it, since "custom schema type" may still be the semantically correct concept for some of these (e.g. routing) even where it's wrong for others (e.g. form rendering). CORE_NODE_TYPESitself may still be legitimate for uses unrelated to frontend-integration presence (e.g. any place that genuinely needs "is this a reserved/protected type name" semantics) — don't remove it wholesale, just stop using it as a proxy for "has a frontend form" if that's not what it means anymore.
Acceptance Criteria
- Opening a
projectnode shows a properties form with all four fields:status,priority,start_date,end_date(via the generic schema-driven form, since no hardcoded form exists forproject) - Existing values (e.g.
status: "planning") display correctly, not just empty controls - Editing a field persists via the existing generic-form update path
-
schema-form-loader.svelte.ts'sloadGenericSchemais called forproject(verify via the loader's own state/logging, not just the rendered output) -
projectchild rows show the "open in other pane" button, consistent with other entity types that lack an inline-editable content area -
projecttab/header titles and navigation routing behave correctly — audited individually againstnavigation-service.ts's two call sites, not assumed to just need the same fix as the others - Every existing core type with a hardcoded form (
task,date, etc.) is unaffected at all 4 call sites — hardcoded forms/viewers still take precedence - A core type with genuinely no schema fields (if any exist) still renders no properties panel — confirm the fix doesn't regress into showing an empty form shell
- No new test failures (
bun run test:all) - Code passes
bun run quality:fix
Technical Specifications
Reference Files
packages/desktop-app/src/lib/design/components/node-type-predicates.ts(lines 13-33) —CORE_NODE_TYPESset andisCustomSchemaType(), the stale predicate itselfpackages/desktop-app/src/lib/design/components/base-node-viewer.svelte(lines 1337-1360) — call site 1, properties-form render gatepackages/desktop-app/src/lib/design/components/schema-form-loader.svelte.ts(around line 62) — call site 2,loadGenericSchemagate; this is the earlier, blocking gate — fixing only call site 1 without this one is insufficientpackages/desktop-app/src/lib/design/components/node-row.svelte(around line 164) — call site 3, inline open-button gatepackages/desktop-app/src/lib/services/navigation-service.ts(lines 137-143, 169-175) — call sites 4a/4b, display-name and navigation-routing gatespluginRegistry(packages/desktop-app/src/lib/plugins/plugin-registry.ts) — likely source of truth for "does this type have a registered form/viewer," used instead of list membership- Schema fields for reference (queried live for
project):status(enum, required, default"planning", core values planning/active/completed/archived/cancelled),priority(enum, optional, low/medium/high),start_date(date, optional),end_date(date, optional) — all stored flat undernode.properties.project.* 35db68b9("Register project as a built-in frontend node type (closes Frontend: register project as a built-in node type (predicates, icon, sidenav) #1754)") — the commit that introduced the core-type-with-no-frontend-integration case this predicate was never designed for; its own message ("Scoped to nav/icon — no viewer or behavior mirror") confirms the gap was known and deferred, not accidental
Non-Goals
- Does not build a hardcoded, purpose-built form for
project— the generic form is sufficient to unblock viewing/editing; a bespoke form is a future UX improvement, not required here. - Does not remove
CORE_NODE_TYPES— it may still be the right tool for other semantics (reserved type names, sidebar sectioning); only its use as a proxy for "has frontend integration" is in scope. - Does not add a
/projectslash command or fixprojectinstance creation — that's Default new node content to "Untitled {Type}" for name-as-content core types (project, skill) #1995. - Does not implement
QueryNodeViewerreactivity for externally-created nodes — that's QueryNodeViewer doesn't reactively add externally-created nodes matching its query #1996.
Related Issues
- Default new node content to "Untitled {Type}" for name-as-content core types (project, skill) #1995 (project has no creation path — same underlying gap:
projectwas never given frontend plugin registration) - QueryNodeViewer doesn't reactively add externally-created nodes matching its query #1996 (found alongside this while manually verifying
projectend-to-end)
Activity
[-]Opening a project node shows no properties form at all[/-][+]Generic schema-driven properties form is wrongly scoped to custom types only[/+][-]Generic schema-driven properties form is wrongly scoped to custom types only[/-][+]isCustomSchemaType conflates "is core" with "has a dedicated frontend integration" across 4 call sites[/+]malibio commentedon Aug 7, 2026
Baseline established on worktree branch. Frontend: 191 test files passed, 4439 tests passed.
Small UI cleanup batch: chat badge, sidenav types, schema-list icons/…
Classify node types by frontend integration, not core-list membership (…
Address review: re-gate title fallback, guard stale schema loads