Appearance
Managing right-sizing rules (admin)
In the admin: Right-sizing rules — the screen this page documents (opens in the running app).
What/why: Right-sizing — what this screen manages and why it exists.
The Right-sizing rules screen under /admin/right-sizing manages the rules the recommendation engine applies — e.g. "high STC requirement → prefer NW Acoustical 645, but surface NW Aluminum 640 / SL45 as cost-effective alternatives". Rules are data, not code, so recommendation guidance stays owned by NanaWall staff. The section is restricted to the rules editor capability (edit_rules) — a data editor is refused; see admin-access.md.
For the rule model itself (conditions, outcomes, priority, how the engine evaluates them), see right-sizing-rules.md. For a read-only picture of how the whole set connects — which answers feed which rules, what each rule recommends or knocks out, and what nothing references — see the structure graph at /admin/structure (#1727).
The rules list (/admin/right-sizing)
The list shows every active rule with its key, priority (evaluation order, lower first), condition count, an outcome summary (preferred + alternatives), and its enabled/disabled status.
- New rule reveals a create form: name, an optional key (defaults to a slug of the name; unique, kebab-case, immutable once created), a required rationale (the "why we recommend this" shown to buyers), an optional internal description, and a priority.
- A new rule is born as a disabled draft with no conditions or outcome. Author those on its edit screen (Edit), then enable it.
- Clone duplicates an existing rule — its core fields, full condition set, and ordered outcomes — into a new draft, so you can tweak a near-complete rule instead of re-authoring from scratch. The clone gets a fresh id (source id +
-copy), is born a disabled(copy)draft, and is library-only (in no ruleset): it inherits neither the source's enabled state nor its memberships and fires nowhere until you review it, enable it, and add it to a ruleset explicitly. It is fully independent — editing or deleting the clone never affects the source. Cloning drops you in the clone's editor.
Draft a whole rule from a prompt (AI, opt-in)
On the New rule page, the Draft with AI panel takes a prose description of a buyer segment — e.g. "interior buyers who prioritize acoustic performance" — and generates a complete draft rule: conditions, ranked outcome systems, and a rationale. It is saved as a disabled draft you then review, edit, and enable — the assist never enables a rule.
Constrained to your catalog — the model can't invent an id. The generator is handed the exact catalog of your existing factor-option / attribute / system ids as its vocabulary, and the result is re-checked server-side: any id the model proposed that isn't in the catalog is dropped (and reported to you — "N proposed ids were omitted"), so a hallucinated id can never reach a rule. The constrained draft must then pass the same validateRule check the enable gate uses; an invalid generation is rejected, not shown — you never get an un-saveable proposal. A missing preferred system is repaired by promoting the top outcome.
Opt-in, fails loud. Prompt drafting is OFF by default behind the RULE_DRAFT_AI intelligence flag (independent of the rationale-drafting flag — the two AI tiers roll out separately; see the rollout inventory and activating intelligence). The model is the shared rule-suggest task profile (/admin/ai). While off, the panel shows a plain notice instead of the prompt box — it tells the admin the assist isn't enabled on this site yet, that manual authoring is fully supported, and to check /admin/ai or contact the site operator to get it enabled (#1729).
Tip: after the draft opens in the editor, use Suggest systems and Draft rationale from corpus (below) to refine its outcome and ground its rationale in product literature.
Cold-start sweep — propose an initial rule set (AI, opt-in)
From the rules list, Cold-start sweep (/admin/right-sizing/sweep) turns an empty or thin rule base into a reviewable starting set in one pass. It proposes one draft rule per factor-option cluster that has attribute expectations (the option's #1477 attribute mappings) and a non-empty deterministic shortlist — reusing the same Suggest systems scorer, not a parallel generator. Each proposal is a disabled draft with provenance; nothing reaches buyers until you accept it.
Review flow (accept / edit / reject). Proposals land in the sweep page's review queue:
- Accept — validates the draft, enables it, and publishes it into Master so it goes live. An edited-to-invalid draft is refused (edit it first) — never enabled.
- Edit — opens the draft in the normal rule editor; refine conditions/outcomes (and ground its rationale with Draft rationale from corpus), then accept.
- Reject — the proposal is remembered so the sweep never re-proposes that cluster, and the disabled draft is retired.
Idempotent + bounded, never silent. Re-running the sweep skips already-proposed clusters (a proposed_for_hash over the cluster's condition signature guards against duplicates, whatever the prior proposal's status). Coverage is reported, not truncated: after a run the page states how many clusters it proposed for and how many it skipped and why (already proposed, or no suggested systems / thin attribute data). The review queue also shows accept / reject counts and the accept rate — the bar the sweep must clear to beat authoring from scratch.
Opt-in, fails loud. The sweep is OFF by default behind the RULE_SWEEP_AI intelligence flag (independent of the other AI-rule flags; see the rollout inventory). While off, the Run sweep button is replaced with a plain notice pointing the admin at /admin/ai or the site operator — never deploy instructions.
Rulesets (/admin/right-sizing, #1268)
Rules are a shared library grouped into rulesets (#1326): a rule is authored once and can belong to many rulesets. A track (persona path) resolves right-sizing against its assigned ruleset; a track with no assignment — and every buyer outside a track — uses the current default ruleset (a movable designation, see below). See the resolution path for how a buyer's track → ruleset → fire-set is resolved.
Master and the movable default (#1639). Two distinct markers appear in the panel:
- Master (badge
master) is the seeded set — the complete catalog of published rules. Every canonical rule placed in any ruleset automatically also becomes a Master member, and a rule's Master membership cannot be removed (retire the rule instead — the Remove action is hidden in the Master scope). Copy-flow clones (#1269) are the exception: they stay in their own copied set and never auto-join Master, which keeps Master the canonical catalog rather than a pile of near-duplicates. Master can never be retired. - Default (badge
default) is a movable pointer, not an identity: exactly one active set holds the designation at a time, and it is what the default/selectflow and every NULL-assigned track inherit. It starts on Master.
The Rulesets panel at the top of the list lets a rules editor:
- Switch the scoped ruleset — the rules table below always shows one ruleset's rules. The selected ruleset is highlighted; each shows its status and rule count.
- Create a ruleset (a name; its id is a kebab slug of the name, immutable once created).
- Rename a ruleset — changes only the display name; the id/key is stable.
- Set as default — moves the default designation to any active set. The confirm states the blast radius (the default
/selectflow plus the N tracks inheriting via a NULL assignment change behavior immediately) and the move is recorded in the audit log. Draft and retired sets are not eligible. - Retire a ruleset (soft) — it is no longer assignable or resolvable, but its rules are preserved. Any track assigned to it falls back to the current default. Retiring is refused for Master (the complete catalog and resolution anchor) and for the current default — move the designation first. Both render as Protected with no retire control.
- Restore a retired ruleset (#1738) — the recovery path for a mistaken Retire: the set flips back to active and is assignable/resolvable again. Restore deliberately does not re-point tracks back (retire's re-point is one-way) — re-assign tracks from the track editor. Only a retired set can be restored; anything else is refused with a clear error. Recorded in the audit log like every other lifecycle action.
- Delete a retired ruleset (#1739) — permanent and irreversible, available only on retired sets (retire → then delete), so Master and the current default are transitively protected: neither can ever be retired. The confirm states what goes: the set's rule memberships are removed, while the member rules survive in the shared library. Any track still pointing at the deleted set returns to the default fallback automatically. Audited.
New rules land in the scoped ruleset. Creating a rule while a ruleset is selected adds it to that ruleset's membership (the create form's Ruleset selector, pre-filled from the list). An ordinary rule edit never changes a rule's memberships.
Reuse a rule across rulesets (Add an existing rule / Remove from ruleset, #1326). Because rules are a shared library, you can put the same rule in more than one ruleset instead of duplicating it:
- Add an existing rule — a picker above the rules table offers library rules not yet in the scoped ruleset. Adding one creates a membership: the same authored rule now fires here too, and any later edit to it stays reflected in every ruleset that uses it. (Retired rules are excluded; disabled drafts are offered, labelled.)
- Remove from ruleset — the per-row Remove action removes the rule's membership only. The rule is not deleted — it stays in the library and in every other ruleset that uses it. Removing its last non-Master membership leaves it a Master-only catalog entry. Removing a Master membership is refused (#1639) — the action is hidden in the Master scope; retiring the rule is the way to take it out of circulation.
- The rule editor shows "used in N rulesets", and when a rule is in more than one it warns that edits are shared — change the rule and every ruleset that uses it changes with it.
Reuse vs. fork. Membership is reuse (one rule, shared). When you instead want a ruleset to diverge, use Copy below — it deep-clones into fresh, independent rules so edits no longer propagate. Effective priority is per-ruleset: a membership may carry its own priority override, so a shared rule can rank differently in different rulesets (see Engine resolution).
Copy a ruleset (#1269) to start a new track's logic from a proven baseline. Copy performs a deep clone into a new independent draft (fresh rules with their own memberships in the new ruleset — the deliberate fork-and-diverge path, distinct from the reuse above):
- A new ruleset is created with status draft and a derived name (
"<source> (copy)"); its id is a unique slug (a second copy gets…-copy-2, etc.). - Every member rule is cloned under a fresh id, carrying its full condition set and ordered outcome verbatim.
enabled,priority, andrationaleare preserved; a retired source rule copies across as retired (nothing is silently dropped). - The copy is independent — editing or retiring a rule in the copy never touches the source ruleset or its rules. Cloning reuses the ordinary rule write path, so a cloned rule is exactly what the engine would reconstruct.
- What is not carried: the source's
draft/active/retiredruleset status (a copy is always a fresh draft) and any track assignments (a copy starts unassigned — assign it from a track editor when ready).
Conflict detection is per-ruleset. The conflict advisory (two enabled rules that co-fire but prefer different systems) is computed over the selected ruleset's members — the set of rules that can actually co-fire for a buyer on it — so two rules that are never in the same ruleset are never falsely flagged.
Every ruleset mutation is gated on the edit_rules capability and recorded in the audit trail, consistent with the rule lifecycle actions.
Editing a rule (/admin/right-sizing/[id])
The edit screen has three parts. The key is immutable once created (it is the stable id the audit trail and engine reference).
Details
Rename the rule, edit its rationale (the "why we recommend this" line buyers see), its internal description, and its priority (evaluation order, lower first).
Effect: recommend vs. exclude (#1719)
A rule's effect is chosen at creation — the new-rule form (/admin/right-sizing/new) offers the Effect selector, so a rule meant to exclude is born an exclusion draft with no post-create flip needed (#1768). On an existing rule, the same selector sits above the Details form on the Overview tab and sets what a fired rule does:
- Recommend (the default) — the rule contributes its outcome systems as candidates. Every rule you had before this selector existed is a recommend rule and behaves exactly as it always has.
- Exclude — the rule removes systems from the result instead. When the buyer's answers match its answer conditions, any system satisfying all of its attribute conditions is knocked out — even if other rules recommend it. Exclusion always beats recommendation, regardless of priority.
When to use an exclusion rule: when an answered question is a hard requirement that some systems simply cannot meet. The engine is otherwise additive — a fired rule can only add candidates — so without an exclusion a system that fails a stated hard requirement can still surface via some other fired rule.
The worked example (swing door): the buyer answers Swingdoor needed: Yes. The recommend rule seed-r-swing-door surfaces the systems with swing-door integration — but the always-on versatile-default rule also fires and would happily surface systems with none. The exclusion twin (seed-r-swing-door-knockout in seed data) has the same answer condition (Swingdoor: Yes) plus the attribute condition Swing door integration = 0: any system with no swing-door option is removed from the result, no matter which rule recommended it.
An exclusion rule uses the same Conditions tab (answer conditions decide when it fires; attribute conditions decide which systems it knocks out) but has no outcome — the Outcome tab explains what the rule excludes instead, and lists the concrete knockout set (#1770): every system (name + id) matching all of the rule's saved attribute conditions, computed with the engine's own operator semantics so the list always agrees with runtime. A condition set matching no system shows an explicit "no system currently matches" empty state. Note the nuance: this is the full static set against the current catalog — at answer time only systems that actually accumulated as candidates are removed, so it is not a prediction of any one buyer's result. A rule with no attribute conditions excludes every system when it fires, and the editor warns about that (no list is rendered). Buyers never see an error from an exclusion — the result simply shrinks (an everything-excluded selection gets the standard explained-empty handling). Use the Preview tab to see which systems a given selection knocks out and by which rule.
Flipping the effect on an enabled rule is guarded like every other structural edit: a live rule must stay valid under its new effect (an enabled rule with outcomes cannot flip to exclude — remove the outcomes or disable first). Flipping a disabled rule to exclude clears any lingering outcome systems in the same update (#1768) — an exclusion rule can't carry outcomes, and before this behavior the hidden leftovers invisibly blocked re-enabling. The flip (and any outcome clearing) is recorded in the audit trail with the effect. Exclusion rules are marked with an Excludes badge on the rules list, and clone/copy paths carry the effect. Suggest-from-conditions and AI rationale drafting are recommend-rule affordances and are hidden on exclusion rules.
Draft rationale (AI, opt-in)
Below the Details form, Draft from corpus writes a first-draft rationale for the rule's outcome systems, grounded in real NanaWall product literature with visible citations (it reuses the same corpus-evidence layer the buyer results surface uses). The draft lands in the Rationale field above as an editable suggestion — review and edit it, then Save changes. Nothing is saved automatically, and the cited passages it drew from are listed beneath the button so you can check the grounding.
Grounded-or-nothing. If the corpus holds no supporting material for the rule's systems, the assist reports "no supporting material" and proposes no rationale rather than inventing one — consistent with the rest of the intelligence surfaces. Populate the corpus (or choose outcome systems with coverage) to get a draft; otherwise write the rationale by hand.
How the model is configured. The draft is generated through the rule-suggest AI task profile — its model is set in /admin/ai (env override RULE_SUGGEST_MODEL, built-in claude-opus-4-8), the same registry-validated profile mechanism every other AI task uses.
Opt-in, fails loud. Rationale drafting is OFF by default behind the RULE_RATIONALE_AI intelligence flag (enumerated in the intelligence rollout inventory; flip it on only after its eval reports GO and with sign-off — see activating intelligence). While off, the Draft from corpus button is replaced with a plain notice directing the admin to /admin/ai or the site operator — an admin surface never silently no-ops.
Conditions
A rule fires only when all of its conditions hold. Two kinds, mirroring the rule model:
- Answer condition — the buyer selected a specific guided-flow factor option (picked from the live factor catalog, e.g. Geography: Coastal).
- Attribute condition — a product attribute meets a threshold: pick an attribute, a comparison operator (
>=,<=,>,<,=,!=,includes), and a threshold value (e.g. Acoustic rating STC ≥ 50).
Outcome
The ordered list of systems the rule recommends. Each entry is preferred (the recommendation) or an alternative (the cost-effective option surfaced alongside, with its budget tier shown). Reorder with ↑/↓ — rank 1 is presented first.
Changing a role in place (#1710): the role marker on each row is a live select — switch it between preferred and alternative and the change saves immediately, keeping the row's position in the order. No remove + re-add needed. The next Preview run reflects the new role (Preview always simulates saved state).
Guardrail: an enabled rule must stay valid — removing its last condition or its only preferred system is refused, and so is demoting its only preferred system to alternative; disable the rule first if you need to restructure it. Disabled drafts can hold any incomplete state. Pickers only offer active (non-retired) factors, attributes, and systems; existing references to since-retired entities still render (by id) until you remove them.
Suggest systems
Below the outcome list, Suggest from conditions computes a ranked shortlist of candidate systems straight from the structured catalog — no AI, no corpus, so it is fully explainable (it works even with an empty corpus). Use it to find the systems that actually fit a rule's requirements instead of recalling them by hand.
Because it ranks by the rule's attribute conditions (see below), it needs at least one. On a rule with zero attribute conditions the button is disabled and the section says so up front — answer (option) conditions test the buyer's selection, not a product, so they don't drive suggestions. Add an attribute condition on the Conditions tab and the button enables.
How the ranking is derived: each active system is scored by how many of the rule's attribute conditions it satisfies, evaluated with the exact same logic the live recommendation engine uses — so a suggestion can never disagree with how the rule would fire. The list is ordered:
- Match count, descending — a system that meets all attribute conditions (a "matches all" badge) outranks one that meets only some ("matches n/N").
- Budget tier, cheapest first — the cheapest sufficient system leads, mirroring cost-tier right-sizing; a system with no tier sorts last within its match count.
- System id, as a final stable tie-break.
Each suggestion shows why it was proposed — the attribute conditions it matched and the system's actual value for each — so there are no opaque scores. Only attribute conditions drive the ranking: answer (option) conditions test the buyer's selection, not a product, so a rule with only answer conditions returns nothing (add an attribute threshold to get suggestions). Systems already in the outcome are excluded, and retired systems never appear.
Nothing is added automatically. Each suggestion carries a role picker (preferred/alternative); adding one uses the same Add system action as the manual form. The list persists across adds (#1716) — add several systems from one run; each added system simply drops off the list. When every suggestion has been added the section says so. Re-run Suggest from conditions only when you want a fresh shortlist (e.g. after changing the rule's conditions).
Rule lifecycle: enable/disable vs. retire
- Enable / Disable is the soft on/off switch. Only enabled rules fire in the engine. Enabling is validated: a recommend rule missing conditions or a preferred system cannot be enabled — and an exclusion rule needs at least one condition but must have no outcomes (#1719) — the toggle is refused with the reason, so an incomplete draft can never reach buyers.
- Retire is a soft delete: the rule stops firing and moves to the Retired section, but its conditions and outcome are preserved; Restore returns it (it comes back with whatever enabled state it had).
- Delete (#1739) is the permanent, irreversible second step, available only on retired rules — an active rule must be retired first, so a mis-click can always be undone before anything is lost. The explicit confirm states the blast radius: the rule's conditions, outcome, and every ruleset membership are removed (schema cascades); other rulesets' memberships of other rules are untouched.
Every create / enable / disable / retire / restore / delete is recorded in the audit trail.
Lifecycle actions live on both surfaces (#1740). Besides the rules list, the rule detail page header offers Clone always, Retire while the rule is active, and Restore + Delete while it is retired — posting to the same list-page server actions (one implementation, two surfaces). Retire/Restore swap the badge and buttons in place; Clone lands you on the new copy's detail page; Delete returns you to the list.
What this maps to
- List / create / enable / disable / retire / restore / clone →
src/routes/admin/right-sizing/+page.server.ts. Single-rule clone reuses the pure builderssingleRuleClonePlan/singleRuleCloneId(ruleset-copy.ts) + the sharedcreateRulewrite path. - Edit + conditions + outcome →
src/routes/admin/right-sizing/[id]/ - Suggest-systems scoring core →
src/lib/server/admin/suggest-systems.ts(pure, deterministic; ranks againstsystem_attribute_valuesvia the engine'sevaluateAttributeCondition). ThesuggestSystemsform action insrc/routes/admin/right-sizing/[id]/+page.server.tsis read-only andedit_rules-gated; applying a suggestion reuses theaddOutcomeaction. - Cold-start sweep (AI) → pure planner + hashing in
src/lib/server/admin/rule-sweep.ts(idempotency + bounded coverage), the generatorrunRuleSweep(rule-sweep-run.ts, composes #1477 mappings +suggestSystems), and the review spinerule-proposals.ts(acceptRuleProposalvalidates→enables→publishes,rejectRuleProposalremembers+retires,ruleProposalMetrics= accept-rate). Persisted in therule_proposalstable (migration 0078). Route:right-sizing/sweep/, flag-gated (RULE_SWEEP_AI). - Prompt-to-rule draft (AI) → pure id-constraint + validation core in
src/lib/server/admin/rule-draft.ts(constrainAndValidateDraftdrops non-catalog ids and runsvalidateRule; invalid → rejected). ThedraftFromPromptaction (right-sizing/new/+page.server.ts) isedit_rules-gated, flag-gated (RULE_DRAFT_AI, fails loud), and creates a disabled draft viacreateRule. Model via therule-suggestprofile over the chat transport. - Rationale drafting (AI) → pure grounding-input shaper + flag resolver in
src/lib/server/admin/rationale-draft.ts; thedraftRationaleform action (+page.server.ts) isedit_rules-gated, flag-gated (RULE_RATIONALE_AI, fails loud), and grounded-or-nothing. Evidence viarecommendation-evidence.ts(#455); model via therule-suggestprofile (ai-task-profiles.ts) over the chat transport (nanasage/chat.ts). The draft populates the Rationale field; the operator saves it throughupdateCore. - Persistence + validation →
src/lib/server/admin/rules.ts(validateRuleCorefor drafts,validateRulefor the enable gate and the enabled-rule guardrail) - Read layer →
getRulesinsrc/lib/server/queries.ts— the engine reads only enabled, non-retired rules; the admin passes{ includeRetired: true, includeDisabled: true }to see everything.