Importing the same site twice through the Super Import wizard leaves two copies of every ambient rule (body, h1, .a .b, …) in site.styleRules, and a third import leaves three. Class rules are reconciled (auto-rename); ambient rules are appended unconditionally, so the registry grows by the full ambient rule count on every re-import.
Reproduce: import a site folder, then import the same folder again into the same site, accepting the default conflict resolutions.
With the repo's own sample fixture (src/__tests__/siteImport/fixtures.ts), driving buildImportPlan → applyConflictResolutions → commitImportPlan twice against one SiteDocument:
ambient after import 1: { h1: 1, body: 1 }
ambient after import 2: { h1: 2, body: 2 }
Both plans carry the identical ambient rules — the second import is not a no-op anywhere in the pipeline.
Why it gets through. Nothing on the whole-site path compares an incoming ambient rule to what the site already has.
detectRuleConflicts opts ambient rules out by design (src/core/siteImport/conflicts.ts:148-149, documented at :9-12 as "Ambient rules never conflict"):
for (const rule of styleRules) {
if (rule.kind !== 'class') continue // ambient rules never conflict
So commitStyleRules finds no conflict row for them and falls through to the add branch (src/core/siteImport/commitPlan.ts:296-310):
const conflict = rule.kind === 'class'
? ruleConflictsByName.get(rule.name)
: undefined
...
id = tx.addStyleRule(rule)
And the transaction's addStyleRule mints a fresh nanoid() entry with no lookup at all (src/admin/pages/site/store/slices/site/helpers.ts:391-407). Because it draws from createStyleRuleOrderAllocator (src/admin/pages/site/store/slices/site/importLinking.ts:21-31, seeded at helpers.ts:355), each duplicate also lands at a higher order than the rule it duplicates, so the newest copy wins the cascade and the older one becomes dead weight that still ships.
The skip-on-duplicate policy exists but is unreachable here. mergeImportedStyleRules does exactly the right comparison (importLinking.ts:129-131):
} else if (ambientSelectors.has(rule.selector)) {
continue // identical ambient selector already present
}
but its only caller is insertImportedNodes (src/admin/pages/site/store/slices/site/nodeActions.ts:226), the single-page fragment-paste path. The Super Import wizard commits through mutateAllPagesAndSite (src/admin/modals/SiteImport/shared/createSiteImportAdapter.ts:168-173), which never calls it. The two import entry points therefore ship two different duplicate policies for the same rules.
Suggested fix: give committed rules a durable source identity and reconcile against it at commit time, rather than adding a second selector-only skip. The plan already knows each rule's origin — plan.styleRuleSources is index-aligned with plan.styleRules and holds the stylesheet FileMap key (src/core/siteImport/types.ts:541-547) — but it is explicitly not persisted onto the committed StyleRule. Persisting that origin (source key plus the rule's ordinal within it) would let commitStyleRules recognise "this is that rule arriving again" and replace it in place, keeping its order, while a rule the user authored or a rule from a different stylesheet under the same selector stays untouched. Selector alone is not a safe identity: a stylesheet may legitimately declare one selector several times, and an ambient rule under a shared selector may not have come from this import at all. Whatever identity is chosen should also be adopted by mergeImportedStyleRules so both import paths agree.
There is no re-import coverage today: src/__tests__/siteImport/applyImport.test.ts exercises every commitImportPlan conflict branch against a fresh site but never commits twice, and src/__tests__/siteImport/conflicts.test.ts only covers class-name collisions — a second-import idempotence case (ambient counts unchanged, repeated selectors within one stylesheet preserved, user-authored ambient rules untouched) belongs in applyImport.test.ts.
Verified at db75118e.
Importing the same site twice through the Super Import wizard leaves two copies of every ambient rule (
body,h1,.a .b, …) insite.styleRules, and a third import leaves three. Class rules are reconciled (auto-rename); ambient rules are appended unconditionally, so the registry grows by the full ambient rule count on every re-import.Reproduce: import a site folder, then import the same folder again into the same site, accepting the default conflict resolutions.
With the repo's own sample fixture (
src/__tests__/siteImport/fixtures.ts), drivingbuildImportPlan→applyConflictResolutions→commitImportPlantwice against oneSiteDocument:Both plans carry the identical ambient rules — the second import is not a no-op anywhere in the pipeline.
Why it gets through. Nothing on the whole-site path compares an incoming ambient rule to what the site already has.
detectRuleConflictsopts ambient rules out by design (src/core/siteImport/conflicts.ts:148-149, documented at:9-12as "Ambient rules never conflict"):So
commitStyleRulesfinds no conflict row for them and falls through to the add branch (src/core/siteImport/commitPlan.ts:296-310):And the transaction's
addStyleRulemints a freshnanoid()entry with no lookup at all (src/admin/pages/site/store/slices/site/helpers.ts:391-407). Because it draws fromcreateStyleRuleOrderAllocator(src/admin/pages/site/store/slices/site/importLinking.ts:21-31, seeded athelpers.ts:355), each duplicate also lands at a higherorderthan the rule it duplicates, so the newest copy wins the cascade and the older one becomes dead weight that still ships.The skip-on-duplicate policy exists but is unreachable here.
mergeImportedStyleRulesdoes exactly the right comparison (importLinking.ts:129-131):but its only caller is
insertImportedNodes(src/admin/pages/site/store/slices/site/nodeActions.ts:226), the single-page fragment-paste path. The Super Import wizard commits throughmutateAllPagesAndSite(src/admin/modals/SiteImport/shared/createSiteImportAdapter.ts:168-173), which never calls it. The two import entry points therefore ship two different duplicate policies for the same rules.Suggested fix: give committed rules a durable source identity and reconcile against it at commit time, rather than adding a second selector-only skip. The plan already knows each rule's origin —
plan.styleRuleSourcesis index-aligned withplan.styleRulesand holds the stylesheet FileMap key (src/core/siteImport/types.ts:541-547) — but it is explicitly not persisted onto the committedStyleRule. Persisting that origin (source key plus the rule's ordinal within it) would letcommitStyleRulesrecognise "this is that rule arriving again" and replace it in place, keeping itsorder, while a rule the user authored or a rule from a different stylesheet under the same selector stays untouched. Selector alone is not a safe identity: a stylesheet may legitimately declare one selector several times, and an ambient rule under a shared selector may not have come from this import at all. Whatever identity is chosen should also be adopted bymergeImportedStyleRulesso both import paths agree.There is no re-import coverage today:
src/__tests__/siteImport/applyImport.test.tsexercises everycommitImportPlanconflict branch against a fresh site but never commits twice, andsrc/__tests__/siteImport/conflicts.test.tsonly covers class-name collisions — a second-import idempotence case (ambient counts unchanged, repeated selectors within one stylesheet preserved, user-authored ambient rules untouched) belongs inapplyImport.test.ts.Verified at
db75118e.