Appearance
Structure graph (/admin/structure)
A read-only, interactive map of how the guided selection actually connects: factors → answers (options) → recommendation rules → systems, laid out as a four-column layered diagram. It exists to answer comprehension questions the individual editing pages can't — which answers does this rule listen to? which systems does nothing recommend? what does this exclusion knock out? — in one view. Authoring stays on the entity pages; every node links to its own edit screen. Tracking issue: #1727 (part of capability #1726).
Access: the Guided selection sidebar group; requires the edit_rules capability (same as Recommendation rules).
Scope: the track selector
The graph is always scoped:
| Selection | Factors shown | Ruleset resolved |
|---|---|---|
| All factors (default) | Every active factor | The default ruleset |
| A specific track | The track's ordered factor list | The track's assigned ruleset (falling back to the default exactly like the live funnel) |
Unknown ?track= ids fail loud with a 404 — admin surfaces never degrade silently.
Rules shown are the active members of the resolved ruleset, plus zero-membership library rules that reference a scoped answer — those would otherwise be invisible precisely when an operator most needs to notice them (they fire nowhere; see flags below).
Reading the graph
Columns are fixed: factors, answers, rules, systems. Row order is chosen to reduce edge crossings (barycenter ordering) and is deterministic for the same data.
Edge kinds (each visually distinct):
- Structure — a factor to its own options.
- Answer — an option to a rule that has an answer condition on it.
- Recommend — a rule to a system in its outcome, annotated with the role (
preferred/alternative #n). - Exclude (knockout) — a rule whose effect is
excludeto the systems it knocks out. Rendered distinctly from recommend edges. - Dependency — a display rule: the source factor to the factor/option it hides ("hides when" / "hides unless").
- Mutual exclusion — the undirected pair of options that exclude each other.
Attribute-threshold conditions are annotations on the rule node (e.g. "Acoustic STC ≥ 50"), not separate nodes — the graph stays legible.
Flags:
- disabled — the rule's toggle is off; it never fires.
- fires nowhere — the rule belongs to zero rulesets (library-only draft).
- orphan — a system no rule recommends (a system only targeted by exclusions is still an orphan), or an option no rule references.
Interaction
- Hover / select a node: its edges and direct neighbors highlight; the rest of the graph dims.
- Click a node: opens the entity's admin edit page (rules →
/admin/right-sizing/<id>, systems →/admin/systems/<id>, factors and options →/admin/factors/<id>). - Wide graphs pan/scroll inside the panel — the page itself never scrolls horizontally.
Tracing a recommendation visually (#1728)
The Trace a recommendation panel above the graph answers "what would happen if a buyer answered like this?" — visually, on the same graph:
- Tick a hypothetical buyer's answers in the picker (options grouped by factor, exactly like the rule editor's Preview tab).
- Run trace. The engine preview (
previewRecommendation— the same evaluation path the buyer funnel uses, no logic fork) runs against the page's current scope: the selected track's resolved ruleset, or the default. - The graph lights the fired path; everything not on it dims:
- Selected answers — accent outline.
- Rules that fired — accent fill; a rule whose conditions matched but whose outcome didn't survive (retired systems, failed attribute thresholds) renders hollow/dashed with a matched — no effect note, so a silently-inert rule is visible at a glance.
- Top pick — solid success outline with a top pick badge; cheaper sufficient alternatives get a dashed success outline. The summary line above the graph flags an over-specified top pick (#61).
- Knockouts — a system removed by an exclusion rule (#1719) renders in danger colors with an excluded by ⟨rule⟩ attribution, and the knockout edge from the excluding rule lights up.
- Change answers and re-run — the trace updates in place (no page reload). Clear trace returns to the plain structure view.
Nothing is saved; the trace is a read-only lens over live data.
Model notes (for developers)
Assembly is a pure, DB-free function over loaded rows — assembleStructureGraph + layoutStructureGraph in src/lib/structure-graph.ts, mirroring the engine's style so the derivation (edge classification, orphan detection, flags, layout) is unit-tested without a database (structure-graph.spec.ts). The server load (src/routes/admin/structure/+page.server.ts) only fetches scoped rows and calls the model. Layout is hand-rolled (fixed four layers + barycenter passes); there is deliberately no graph-layout dependency. The influence trace is the same pattern one layer up: traceHighlights in src/lib/structure-trace.ts (unit-tested in structure-trace.spec.ts) projects the RulePreview payload onto node/edge highlight states; the engine work stays in previewRecommendation.
Dangling option references (#1869)
The graph also reports references that point at an option which no longer exists — damage predating the factor-option delete guard, which nothing else surfaces because neither reference set (tracks.preseeded_answers_json, track_display_rules) has a foreign key to violate.
- A warning panel above the graph lists each finding, linked to the track that owns it. A global display rule (#905) has no track, so it is named "Global rule set" rather than being attributed to one.
- Any factor whose pre-seeded answer is missing carries a
dangling-refflag ("dangling reference") on its node, alongside the existingdisabled/no-ruleset/orphanvocabulary — so the finding lives on the graph, not only in the banner above it.
Scoped to the selected track when one is chosen; global rules always show.
Report only. The repair (drop the pre-seed? re-point the rule?) is an editorial call about what the track means, so nothing here changes anything. The same findings come out of npm run verify:option-refs, which reads the identical lookup as the delete guard — there is exactly one definition of "references an option" ($lib/server/admin/factor-option-refs).