From 9a8b44380d09f5d94f49e3d6c3f773f7c00345df Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Fri, 12 Jun 2026 19:46:14 +0300 Subject: [PATCH] Services: delete dead RelationshipService; rename persistence to snippet-persistence --- docs/architecture/01-state-and-stores.md | 2 +- docs/architecture/02-persistence.md | 2 +- .../07-naming-and-relationships.md | 99 +++++++------------ .../10-interaction-and-feedback.md | 2 +- ...ce.test.ts => snippet-persistence.test.ts} | 14 +-- ...{persistence.ts => snippet-persistence.ts} | 4 +- src/app/orchestration/startup.ts | 4 +- src/app/services/RelationshipService.test.ts | 72 -------------- src/app/services/RelationshipService.ts | 54 ---------- src/app/stores/DatasetStore.test.ts | 32 ++++++ src/app/stores/SnippetStore.ts | 6 +- src/core/relationships.ts | 4 +- 12 files changed, 86 insertions(+), 209 deletions(-) rename src/app/orchestration/{persistence.test.ts => snippet-persistence.test.ts} (92%) rename src/app/orchestration/{persistence.ts => snippet-persistence.ts} (95%) delete mode 100644 src/app/services/RelationshipService.test.ts delete mode 100644 src/app/services/RelationshipService.ts diff --git a/docs/architecture/01-state-and-stores.md b/docs/architecture/01-state-and-stores.md index d19760c..be7789b 100644 --- a/docs/architecture/01-state-and-stores.md +++ b/docs/architecture/01-state-and-stores.md @@ -398,7 +398,7 @@ every keystroke. A startup subscriber observes the draft and debounces the expen work: ```ts -// src/app/orchestration/persistence.ts +// src/app/orchestration/snippet-persistence.ts import { useSnippetStore } from '../stores/SnippetStore'; import { saveSnippet } from '../infrastructure/snippet-store'; // IndexedDB adapter diff --git a/docs/architecture/02-persistence.md b/docs/architecture/02-persistence.md index 2ccd63a..50fb4d9 100644 --- a/docs/architecture/02-persistence.md +++ b/docs/architecture/02-persistence.md @@ -451,7 +451,7 @@ export async function saveSnippet(s: Snippet): Promise { fire-and-forget `void saveSnippet(n)` re-buries the very error the adapter took care to throw. Persistence write-backs are wired as store subscribers, so the surfacing path is: -`orchestration/persistence.ts` (write-through `.catch`) / `orchestration/startup.ts` (load +`orchestration/snippet-persistence.ts` (write-through `.catch`) / `orchestration/startup.ts` (load `.catch`, then run in memory) → `services/storage-errors.ts` (pure error→message mapper) → `notify()` (`stores/NotificationStore`) → `Toaster`. diff --git a/docs/architecture/07-naming-and-relationships.md b/docs/architecture/07-naming-and-relationships.md index 7824db1..d7bc136 100644 --- a/docs/architecture/07-naming-and-relationships.md +++ b/docs/architecture/07-naming-and-relationships.md @@ -260,22 +260,14 @@ export function datasetUsageCounts( ): Map; ``` -```ts -// src/app/services/RelationshipService.ts — snapshot wrapper for non-reactive callers - -import { snippetsReferencingDataset } from '../../core/relationships'; -import { useSnippetStore } from '../stores/SnippetStore'; - -export function findSnippetsReferencingDataset(name: string): Snippet[] { - return snippetsReferencingDataset(useSnippetStore.getState().snippets, name); -} -``` - -The reactive UI does **not** go through the service: a component subscribed to -`snippets` calls the core helper directly, so the badge and Linked Snippets list -update the moment any snippet's draft changes its refs (auto-save) or it is -published — no duplicated matching logic, no `getState()` snapshot that would -miss updates. +There is no app-layer wrapper module around the scan. The reactive UI — a +component subscribed to `snippets` — calls the core helper directly, so the +badge and Linked Snippets list update the moment any snippet's draft changes +its refs (auto-save) or it is published. A non-reactive caller binds the same +helper to a snapshot inline at its call site +(`snippetsReferencingDataset(useSnippetStore.getState().snippets, name)`) — +one matching implementation, no `getState()` wrapper that would go stale in +reactive code. **Do** @@ -283,7 +275,7 @@ miss updates. reactive and snapshot callers share it. It is O(snippets) but the collections are small (library budget ~5 MB); clarity beats an index. - Call the core helper directly from a store-subscribed component for reactivity; - use the service wrapper only from non-reactive (snapshot) code. + bind it to a `getState()` snapshot inline for non-reactive code. **Don't** @@ -334,9 +326,10 @@ export function dedupeIncomingDatasetNames( ``` > If imported snippets reference the renamed dataset, their `datasetRefs` and -> specs must be rewritten to the new name too — reuse the rename machinery in -> §6 over the imported snippet set, or run `renameDatasetEverywhere` per applied -> rename after the import is committed. +> specs must be rewritten to the new name too. The import flow does this purely, +> before anything is committed: `applyDatasetRenamesToSnippets` +> (`core/import-normalize`) rewrites the incoming snippet set per applied rename +> using the same `renameDatasetInSpec` machinery as §6. ### 5.1 Where the import/export flow lives, and its rules @@ -427,43 +420,22 @@ export function renameDatasetInSpec(spec: Json, oldName: string, newName: string } ``` -```ts -// src/app/services/RelationshipService.ts — thin store coordinator - -import { useDatasetStore } from '../stores/DatasetStore'; -import { useSnippetStore } from '../stores/SnippetStore'; - -/** - * Renames a dataset and propagates the rename to every referencing snippet - * (spec, draftSpec, and datasetRefs). Returns the number of snippets changed. - * Caller is responsible for collision policy on `newName` (reject vs suffix). - */ -export function renameDatasetEverywhere( - oldName: string, - newName: string, - now?: Date, -): { updated: number } { - if (oldName === newName) return { updated: 0 }; - - // 1. Rename the dataset record itself. - const dataset = useDatasetStore.getState().datasets.find((d) => d.name === oldName); - if (dataset) useDatasetStore.getState().update(dataset.id, { name: newName }, now); - - // 2 + 3. Delegate the spec/draftSpec/refs rewrite to the ONE rename impl — the - // store action that owns the snippet collection. It matches by spec+draft - // *content* (not `datasetRefs`, which mirrors only the draft) and recomputes - // `datasetRefs` from the rewritten draft. Keeping the loop in the store means - // the reactive editor buffer is refreshed in the same atomic update when the - // active snippet's draft is rewritten. - return { updated: useSnippetStore.getState().renameDatasetRefs(oldName, newName, now) }; -} -``` +The one rename implementation in the live app is the store action +**`SnippetStore.renameDatasetRefs(oldName, newName, now)`** — the action that +owns the snippet collection. It matches by spec+draft _content_ (not +`datasetRefs`, which mirrors only the draft), rewrites `spec`/`draftSpec` via +`renameDatasetInSpec`, recomputes `datasetRefs` from the rewritten draft, and +returns the number of snippets changed. Keeping the loop in the store means the +reactive editor buffer is refreshed in the same atomic update when the active +snippet's draft is rewritten. `DatasetStore.update` calls it whenever a save +changes a dataset's name, so a rename propagates everywhere as part of the one +user action — there is no separate coordinator module to call. > **Collision on rename.** The UI rename form rejects a name already in use via > `isNameTaken(newName, datasets, dataset.id)`. Programmatic renames (e.g. an -> import flow) instead resolve with `makeUniqueName` before calling -> `renameDatasetEverywhere`. The propagation function itself does not invent a -> name — it assumes `newName` is the agreed target. +> import flow) instead resolve with `makeUniqueName` first. The propagation +> action itself does not invent a name — it assumes `newName` is the agreed +> target. **Do** @@ -492,18 +464,17 @@ export function renameDatasetEverywhere( ## 7. Where things live -| Concern | Location | Pure? | Tested | -| ------------------------------------------------------------------------ | ----------------------------------------- | ------------------- | ----------- | -| `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit | -| `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit | -| `renameDatasetInSpec` | `src/core/spec-refs.ts` | yes | unit | -| `snippetsReferencingDataset`, `datasetUsageCounts` (reverse-lookup scan) | `src/core/relationships.ts` | yes | unit | -| `findSnippetsReferencingDataset`, usage count (snapshot wrappers) | `src/app/services/RelationshipService.ts` | no (reads store) | integration | -| `renameDatasetEverywhere` → `{ updated }` | `src/app/services/RelationshipService.ts` | no (mutates stores) | integration | -| `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit | +| Concern | Location | Pure? | Tested | +| ------------------------------------------------------------------------ | -------------------------------- | ------------------- | ----------- | +| `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit | +| `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit | +| `renameDatasetInSpec` | `src/core/spec-refs.ts` | yes | unit | +| `snippetsReferencingDataset`, `datasetUsageCounts` (reverse-lookup scan) | `src/core/relationships.ts` | yes | unit | +| `renameDatasetRefs` → updated count (rename propagation) | `src/app/stores/SnippetStore.ts` | no (mutates stores) | integration | +| `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit | The dividing line: anything that takes plain data and returns plain data is **core** and unit-tested in isolation; anything that reaches into a Zustand store -is an **app service**. The rule of thumb — _the **draft** spec is the source of +is a **store action or app service**. The rule of thumb — _the **draft** spec is the source of truth, `datasetRefs` mirrors it, the reverse lookup is derived_ — is what keeps the bidirectional link from ever needing manual repair. diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md index ff88f6d..5746f1a 100644 --- a/docs/architecture/10-interaction-and-feedback.md +++ b/docs/architecture/10-interaction-and-feedback.md @@ -138,7 +138,7 @@ control and freedom," #5 "error prevention"; spec §10 Reliability). - **No silent data loss.** Edits auto-save as a **draft**; a known-good **published** version is always preserved separately (§03D). A failed persist **surfaces as a toast**, - never a swallowed promise (`orchestration/persistence.ts`). + never a swallowed promise (`orchestration/snippet-persistence.ts`). - **A marked exit from every committed change.** Revert restores the published spec; Escape closes modals; delete/revert/reset require confirmation first. - **Resilient rendering recovers on its own.** An invalid spec shows a readable error and diff --git a/src/app/orchestration/persistence.test.ts b/src/app/orchestration/snippet-persistence.test.ts similarity index 92% rename from src/app/orchestration/persistence.test.ts rename to src/app/orchestration/snippet-persistence.test.ts index 1018edc..57fbaf9 100644 --- a/src/app/orchestration/persistence.test.ts +++ b/src/app/orchestration/snippet-persistence.test.ts @@ -19,7 +19,7 @@ vi.mock('../infrastructure/snippet-store', async (importOriginal) => { import { saveSnippet, deleteSnippet, StorageQuotaError } from '../infrastructure/snippet-store'; import { useNotificationStore } from '../stores/NotificationStore'; import { useSnippetStore } from '../stores/SnippetStore'; -import { AUTOSAVE_DEBOUNCE_MS, wirePersistence } from './persistence'; +import { AUTOSAVE_DEBOUNCE_MS, wireSnippetPersistence } from './snippet-persistence'; const save = vi.mocked(saveSnippet); const del = vi.mocked(deleteSnippet); @@ -51,7 +51,7 @@ afterEach(() => { describe('wireDraftAutoSave', () => { it('persists a settled, valid edit after the debounce', async () => { useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); useSnippetStore.getState().updateDraft('{"a":2}'); @@ -65,7 +65,7 @@ describe('wireDraftAutoSave', () => { it('does not persist a half-typed, unparseable buffer', async () => { useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); useSnippetStore.getState().updateDraft('{ "a":'); @@ -78,7 +78,7 @@ describe('wireDraftAutoSave', () => { describe('wireWriteThrough', () => { it('persists a newly created snippet', () => { useSnippetStore.getState().hydrate([]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); useSnippetStore.getState().createSnippet({ id: 'n', spec: '{}' }); @@ -90,7 +90,7 @@ describe('wireWriteThrough', () => { it('deletes a removed snippet without re-saving the survivors', () => { useSnippetStore.getState().hydrate([snippetWith('a', '{}'), snippetWith('b', '{}')]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); useSnippetStore.getState().removeSnippet('a'); @@ -103,7 +103,7 @@ describe('wireWriteThrough', () => { it('ignores state changes that do not touch the snippets array', () => { useSnippetStore.getState().hydrate([snippetWith('a', '{}')]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); useSnippetStore.getState().setEditorView('published'); @@ -114,7 +114,7 @@ describe('wireWriteThrough', () => { it('surfaces a failed save as an error notification instead of losing it silently', async () => { useSnippetStore.getState().hydrate([]); - teardown = wirePersistence(); + teardown = wireSnippetPersistence(); save.mockClear(); save.mockRejectedValueOnce(new StorageQuotaError()); diff --git a/src/app/orchestration/persistence.ts b/src/app/orchestration/snippet-persistence.ts similarity index 95% rename from src/app/orchestration/persistence.ts rename to src/app/orchestration/snippet-persistence.ts index c5b077d..c91f79a 100644 --- a/src/app/orchestration/persistence.ts +++ b/src/app/orchestration/snippet-persistence.ts @@ -1,5 +1,5 @@ /** - * Persistence & auto-save wiring (docs/architecture/01 §5). + * Snippet persistence & auto-save wiring (docs/architecture/01 §5). * * Bridges the pure SnippetStore to the IndexedDB adapter via startup * subscribers — the store stays browser-free, and all reads/writes funnel @@ -61,7 +61,7 @@ function wireWriteThrough(): Unsubscribe { } /** Wire all persistence subscribers. Returns a teardown that detaches them. */ -export function wirePersistence(): Unsubscribe { +export function wireSnippetPersistence(): Unsubscribe { const unsubs = [wireDraftAutoSave(), wireWriteThrough()]; return () => unsubs.forEach((u) => u()); } diff --git a/src/app/orchestration/startup.ts b/src/app/orchestration/startup.ts index 9209888..d4ecba3 100644 --- a/src/app/orchestration/startup.ts +++ b/src/app/orchestration/startup.ts @@ -19,7 +19,7 @@ import { notify } from '../stores/NotificationStore'; import { useSnippetStore } from '../stores/SnippetStore'; import { useDatasetStore } from '../stores/DatasetStore'; import { useCustomThemeStore } from '../stores/CustomThemeStore'; -import { wirePersistence } from './persistence'; +import { wireSnippetPersistence } from './snippet-persistence'; import { wireDatasetPersistence } from './dataset-persistence'; import { wireThemePersistence } from './theme-persistence'; import { startRouting } from '../modals/UrlStateSync'; @@ -67,7 +67,7 @@ export async function initApp(): Promise { // Wire persistence AFTER hydrate so write-through's baseline is the loaded set // — otherwise it would redundantly re-save every record on each startup. - wirePersistence(); + wireSnippetPersistence(); wireDatasetPersistence(); wireThemePersistence(); diff --git a/src/app/services/RelationshipService.test.ts b/src/app/services/RelationshipService.test.ts deleted file mode 100644 index 8676013..0000000 --- a/src/app/services/RelationshipService.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { beforeEach, describe, expect, test } from 'vitest'; -import { createDataset } from '@core/dataset'; -import { createSnippet } from '@core/snippet'; -import { useDatasetStore } from '../stores/DatasetStore'; -import { useSnippetStore } from '../stores/SnippetStore'; -import { - datasetUsageCount, - findSnippetsReferencingDataset, - renameDatasetEverywhere, -} from './RelationshipService'; - -const T = new Date('2026-06-01T00:00:00Z'); - -/** Seed a snippet whose published spec references `datasetName`. */ -function seedSnippet(id: string, datasetName: string) { - return createSnippet({ - id, - spec: JSON.stringify({ data: { name: datasetName }, mark: 'bar' }), - now: T, - }); -} - -beforeEach(() => { - useDatasetStore.getState().reset(); - useSnippetStore.getState().reset(); -}); - -describe('reverse lookup', () => { - test('finds referencing snippets case-insensitively and counts them', () => { - useSnippetStore.getState().hydrate([seedSnippet('a', 'Sales'), seedSnippet('b', 'SALES')], 'a'); - // datasetRefs are recomputed on publish; publish each active snippet to seed. - useSnippetStore.getState().publish(T); - useSnippetStore.getState().selectSnippet('b'); - useSnippetStore.getState().publish(T); - - expect( - findSnippetsReferencingDataset('sales') - .map((s) => s.id) - .sort(), - ).toEqual(['a', 'b']); - expect(datasetUsageCount('Sales')).toBe(2); - expect(datasetUsageCount('Other')).toBe(0); - }); -}); - -describe('renameDatasetEverywhere', () => { - test('renames the dataset record and every referencing snippet', () => { - useDatasetStore.getState().add( - createDataset({ - name: 'Sales', - data: [{ a: 1 }], - format: 'json', - source: 'inline', - now: T, - }), - ); - useSnippetStore.getState().hydrate([seedSnippet('a', 'Sales')], 'a'); - useSnippetStore.getState().publish(T); - - const result = renameDatasetEverywhere('Sales', 'Revenue', new Date('2026-07-01T00:00:00Z')); - - expect(result).toEqual({ updated: 1 }); - expect(useDatasetStore.getState().datasets[0].name).toBe('Revenue'); - const s = useSnippetStore.getState().snippets[0]; - expect(s.datasetRefs).toEqual(['Revenue']); - expect(s.spec).toContain('"Revenue"'); - }); - - test('is a no-op when old and new names are equal', () => { - expect(renameDatasetEverywhere('Sales', 'Sales')).toEqual({ updated: 0 }); - }); -}); diff --git a/src/app/services/RelationshipService.ts b/src/app/services/RelationshipService.ts deleted file mode 100644 index d1986b7..0000000 --- a/src/app/services/RelationshipService.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Snippet ↔ dataset relationships (docs/architecture/07 §4 + §6). - * - * The bidirectional link is name-based and has a single source of truth: a - * snippet's `datasetRefs` (recomputed from its draft spec — the version being - * edited — on every draft change and on publish). The reverse direction — - * "which snippets use this dataset" — is therefore DERIVED by a scan, - * never stored, so it can't drift. Rename is the one graph operation: it renames - * the dataset record and propagates the new name into every referencing snippet's - * spec/draftSpec/refs. - * - * These functions read and mutate Zustand stores, so they live in the app layer - * (the pure rewrite/extraction helpers they build on live in `core/spec-refs`). - */ - -import type { Snippet } from '@core/snippet'; -import { snippetsReferencingDataset } from '@core/relationships'; -import { useDatasetStore } from '../stores/DatasetStore'; -import { useSnippetStore } from '../stores/SnippetStore'; - -// The reverse-lookup scan itself is the pure `snippetsReferencingDataset` -// (`core/relationships`) — the *one* implementation arch/07 §6 calls for. These -// wrappers just bind it to a store snapshot for non-reactive callers (e.g. a -// programmatic rename); the reactive UI calls the core helper directly with its -// live `snippets` selection, so neither side duplicates the matching logic. - -/** Snippets whose `datasetRefs` include `name` (case-insensitive), from the store. */ -export function findSnippetsReferencingDataset(name: string): Snippet[] { - return snippetsReferencingDataset(useSnippetStore.getState().snippets, name); -} - -/** Count for the dataset usage badge (spec §05 → List item). */ -export function datasetUsageCount(name: string): number { - return findSnippetsReferencingDataset(name).length; -} - -/** - * Rename a dataset and propagate everywhere (docs/architecture/07 §6): renames - * the dataset record, then rewrites every referencing snippet's spec, draftSpec, - * and datasetRefs (via `SnippetStore.renameDatasetRefs`, the single rename impl). - * The caller is responsible for collision policy on `newName` (the edit form - * rejects a taken name; programmatic paths pre-resolve a unique one). Returns the - * number of snippets updated, as `{ updated }` (arch/07 §6). - */ -export function renameDatasetEverywhere( - oldName: string, - newName: string, - now?: Date, -): { updated: number } { - if (oldName === newName) return { updated: 0 }; - const dataset = useDatasetStore.getState().datasets.find((d) => d.name === oldName); - if (dataset) useDatasetStore.getState().update(dataset.id, { name: newName }, now); - return { updated: useSnippetStore.getState().renameDatasetRefs(oldName, newName, now) }; -} diff --git a/src/app/stores/DatasetStore.test.ts b/src/app/stores/DatasetStore.test.ts index 4c19377..9b4671c 100644 --- a/src/app/stores/DatasetStore.test.ts +++ b/src/app/stores/DatasetStore.test.ts @@ -52,6 +52,38 @@ describe('save — create', () => { }); }); +describe('save — rename propagation (docs/architecture/07 §6)', () => { + test('renaming via the edit form rewrites every referencing snippet', () => { + store().startCreate(); + store().updateForm({ name: 'Sales', input: '[{"a":1}]' }); + expect(store().save(T)).toBe(true); + + // A published snippet referencing the dataset by name. + useSnippetStore.getState().hydrate( + [ + createSnippet({ + id: 'a', + spec: JSON.stringify({ data: { name: 'Sales' }, mark: 'bar' }), + now: T, + }), + ], + 'a', + ); + useSnippetStore.getState().publish(T); + expect(useSnippetStore.getState().snippets[0].datasetRefs).toEqual(['Sales']); + + store().startEdit(); + store().updateForm({ name: 'Revenue' }); + expect(store().save(new Date('2026-07-01T00:00:00Z'))).toBe(true); + + expect(store().datasets[0].name).toBe('Revenue'); + const s = useSnippetStore.getState().snippets[0]; + expect(s.datasetRefs).toEqual(['Revenue']); + expect(s.spec).toContain('"Revenue"'); + expect(s.spec).not.toContain('"Sales"'); + }); +}); + describe('commitUrlSnapshot — create from a fetched body', () => { test('snapshots the body, infers format from content, and profiles it', () => { store().startCreate(); diff --git a/src/app/stores/SnippetStore.ts b/src/app/stores/SnippetStore.ts index e35ddb3..4c7f6a7 100644 --- a/src/app/stores/SnippetStore.ts +++ b/src/app/stores/SnippetStore.ts @@ -5,9 +5,9 @@ * active, and the live editor buffer (`draftText`). Actions are the single place * snippet state mutates, so they are unit-testable without a DOM or IndexedDB. * - * Persistence is NOT done here — a startup subscriber (orchestration/persistence) - * observes this store and writes through to the IndexedDB adapter. That keeps the - * store pure and free of browser APIs. + * Persistence is NOT done here — a startup subscriber + * (orchestration/snippet-persistence) observes this store and writes through to + * the IndexedDB adapter. That keeps the store pure and free of browser APIs. * * Draft/published model (spec §03D): every snippet carries a published `spec` * and a working `draftSpec`. Ordinary editing — keystrokes, auto-save — touches diff --git a/src/core/relationships.ts b/src/core/relationships.ts index 5d0a707..a569db6 100644 --- a/src/core/relationships.ts +++ b/src/core/relationships.ts @@ -6,8 +6,8 @@ * scan, never stored, so it can't drift. This module is that scan's single pure * implementation: it takes plain snippet records and a name and returns plain * data, so the same logic serves both the reactive UI (the Dataset Manager passes - * its live `snippets` selection) and the non-reactive `RelationshipService` (which - * passes a store snapshot). One definition, per arch/07 §6 ("'who references this' + * its live `snippets` selection) and any non-reactive caller (which passes a + * store snapshot inline). One definition, per arch/07 §6 ("'who references this' * has exactly one implementation"). * * Matching is **case-insensitive**, mirroring the naming policy (`core/naming`):