Services: delete dead RelationshipService; rename persistence to snippet-persistence

This commit is contained in:
2026-06-12 19:46:14 +03:00
parent 538882b342
commit 9a8b44380d
12 changed files with 86 additions and 209 deletions
+1 -1
View File
@@ -398,7 +398,7 @@ every keystroke. A startup subscriber observes the draft and debounces the expen
work: work:
```ts ```ts
// src/app/orchestration/persistence.ts // src/app/orchestration/snippet-persistence.ts
import { useSnippetStore } from '../stores/SnippetStore'; import { useSnippetStore } from '../stores/SnippetStore';
import { saveSnippet } from '../infrastructure/snippet-store'; // IndexedDB adapter import { saveSnippet } from '../infrastructure/snippet-store'; // IndexedDB adapter
+1 -1
View File
@@ -451,7 +451,7 @@ export async function saveSnippet(s: Snippet): Promise<void> {
fire-and-forget `void saveSnippet(n)` re-buries the very error the adapter took care to 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: 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) → `.catch`, then run in memory) → `services/storage-errors.ts` (pure error→message mapper) →
`notify()` (`stores/NotificationStore`) → `Toaster`. `notify()` (`stores/NotificationStore`) → `Toaster`.
@@ -260,22 +260,14 @@ export function datasetUsageCounts(
): Map<string, number>; ): Map<string, number>;
``` ```
```ts There is no app-layer wrapper module around the scan. The reactive UI — a
// src/app/services/RelationshipService.ts — snapshot wrapper for non-reactive callers component subscribed to `snippets` — calls the core helper directly, so the
badge and Linked Snippets list update the moment any snippet's draft changes
import { snippetsReferencingDataset } from '../../core/relationships'; its refs (auto-save) or it is published. A non-reactive caller binds the same
import { useSnippetStore } from '../stores/SnippetStore'; helper to a snapshot inline at its call site
(`snippetsReferencingDataset(useSnippetStore.getState().snippets, name)`) —
export function findSnippetsReferencingDataset(name: string): Snippet[] { one matching implementation, no `getState()` wrapper that would go stale in
return snippetsReferencingDataset(useSnippetStore.getState().snippets, name); reactive code.
}
```
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.
**Do** **Do**
@@ -283,7 +275,7 @@ miss updates.
reactive and snapshot callers share it. It is O(snippets) but the collections reactive and snapshot callers share it. It is O(snippets) but the collections
are small (library budget ~5 MB); clarity beats an index. are small (library budget ~5 MB); clarity beats an index.
- Call the core helper directly from a store-subscribed component for reactivity; - 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** **Don't**
@@ -334,9 +326,10 @@ export function dedupeIncomingDatasetNames(
``` ```
> If imported snippets reference the renamed dataset, their `datasetRefs` and > If imported snippets reference the renamed dataset, their `datasetRefs` and
> specs must be rewritten to the new name too — reuse the rename machinery in > specs must be rewritten to the new name too. The import flow does this purely,
> §6 over the imported snippet set, or run `renameDatasetEverywhere` per applied > before anything is committed: `applyDatasetRenamesToSnippets`
> rename after the import is committed. > (`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 ### 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 The one rename implementation in the live app is the store action
// src/app/services/RelationshipService.ts — thin store coordinator **`SnippetStore.renameDatasetRefs(oldName, newName, now)`** — the action that
owns the snippet collection. It matches by spec+draft _content_ (not
import { useDatasetStore } from '../stores/DatasetStore'; `datasetRefs`, which mirrors only the draft), rewrites `spec`/`draftSpec` via
import { useSnippetStore } from '../stores/SnippetStore'; `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
* Renames a dataset and propagates the rename to every referencing snippet snippet's draft is rewritten. `DatasetStore.update` calls it whenever a save
* (spec, draftSpec, and datasetRefs). Returns the number of snippets changed. changes a dataset's name, so a rename propagates everywhere as part of the one
* Caller is responsible for collision policy on `newName` (reject vs suffix). user action — there is no separate coordinator module to call.
*/
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) };
}
```
> **Collision on rename.** The UI rename form rejects a name already in use via > **Collision on rename.** The UI rename form rejects a name already in use via
> `isNameTaken(newName, datasets, dataset.id)`. Programmatic renames (e.g. an > `isNameTaken(newName, datasets, dataset.id)`. Programmatic renames (e.g. an
> import flow) instead resolve with `makeUniqueName` before calling > import flow) instead resolve with `makeUniqueName` first. The propagation
> `renameDatasetEverywhere`. The propagation function itself does not invent a > action itself does not invent a name — it assumes `newName` is the agreed
> name — it assumes `newName` is the agreed target. > target.
**Do** **Do**
@@ -492,18 +464,17 @@ export function renameDatasetEverywhere(
## 7. Where things live ## 7. Where things live
| Concern | Location | Pure? | Tested | | Concern | Location | Pure? | Tested |
| ------------------------------------------------------------------------ | ----------------------------------------- | ------------------- | ----------- | | ------------------------------------------------------------------------ | -------------------------------- | ------------------- | ----------- |
| `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit | | `makeUniqueName`, `isNameTaken` | `src/core/naming.ts` | yes | unit |
| `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit | | `extractDatasetRefs`, `recomputeDatasetRefs` | `src/core/spec-refs.ts` | yes | unit |
| `renameDatasetInSpec` | `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 | | `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 | | `renameDatasetRefs` → updated count (rename propagation) | `src/app/stores/SnippetStore.ts` | no (mutates stores) | integration |
| `renameDatasetEverywhere``{ updated }` | `src/app/services/RelationshipService.ts` | no (mutates stores) | integration | | `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit |
| `dedupeIncomingDatasetNames` | `src/core/import-normalize.ts` | yes | unit |
The dividing line: anything that takes plain data and returns plain data is 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 **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 truth, `datasetRefs` mirrors it, the reverse lookup is derived_ — is what keeps
the bidirectional link from ever needing manual repair. the bidirectional link from ever needing manual repair.
@@ -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** - **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**, 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; - **A marked exit from every committed change.** Revert restores the published spec;
Escape closes modals; delete/revert/reset require confirmation first. Escape closes modals; delete/revert/reset require confirmation first.
- **Resilient rendering recovers on its own.** An invalid spec shows a readable error and - **Resilient rendering recovers on its own.** An invalid spec shows a readable error and
@@ -19,7 +19,7 @@ vi.mock('../infrastructure/snippet-store', async (importOriginal) => {
import { saveSnippet, deleteSnippet, StorageQuotaError } from '../infrastructure/snippet-store'; import { saveSnippet, deleteSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
import { useNotificationStore } from '../stores/NotificationStore'; import { useNotificationStore } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore'; 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 save = vi.mocked(saveSnippet);
const del = vi.mocked(deleteSnippet); const del = vi.mocked(deleteSnippet);
@@ -51,7 +51,7 @@ afterEach(() => {
describe('wireDraftAutoSave', () => { describe('wireDraftAutoSave', () => {
it('persists a settled, valid edit after the debounce', async () => { it('persists a settled, valid edit after the debounce', async () => {
useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]); useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
useSnippetStore.getState().updateDraft('{"a":2}'); useSnippetStore.getState().updateDraft('{"a":2}');
@@ -65,7 +65,7 @@ describe('wireDraftAutoSave', () => {
it('does not persist a half-typed, unparseable buffer', async () => { it('does not persist a half-typed, unparseable buffer', async () => {
useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]); useSnippetStore.getState().hydrate([snippetWith('a', '{"a":1}')]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
useSnippetStore.getState().updateDraft('{ "a":'); useSnippetStore.getState().updateDraft('{ "a":');
@@ -78,7 +78,7 @@ describe('wireDraftAutoSave', () => {
describe('wireWriteThrough', () => { describe('wireWriteThrough', () => {
it('persists a newly created snippet', () => { it('persists a newly created snippet', () => {
useSnippetStore.getState().hydrate([]); useSnippetStore.getState().hydrate([]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
useSnippetStore.getState().createSnippet({ id: 'n', spec: '{}' }); useSnippetStore.getState().createSnippet({ id: 'n', spec: '{}' });
@@ -90,7 +90,7 @@ describe('wireWriteThrough', () => {
it('deletes a removed snippet without re-saving the survivors', () => { it('deletes a removed snippet without re-saving the survivors', () => {
useSnippetStore.getState().hydrate([snippetWith('a', '{}'), snippetWith('b', '{}')]); useSnippetStore.getState().hydrate([snippetWith('a', '{}'), snippetWith('b', '{}')]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
useSnippetStore.getState().removeSnippet('a'); useSnippetStore.getState().removeSnippet('a');
@@ -103,7 +103,7 @@ describe('wireWriteThrough', () => {
it('ignores state changes that do not touch the snippets array', () => { it('ignores state changes that do not touch the snippets array', () => {
useSnippetStore.getState().hydrate([snippetWith('a', '{}')]); useSnippetStore.getState().hydrate([snippetWith('a', '{}')]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
useSnippetStore.getState().setEditorView('published'); 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 () => { it('surfaces a failed save as an error notification instead of losing it silently', async () => {
useSnippetStore.getState().hydrate([]); useSnippetStore.getState().hydrate([]);
teardown = wirePersistence(); teardown = wireSnippetPersistence();
save.mockClear(); save.mockClear();
save.mockRejectedValueOnce(new StorageQuotaError()); save.mockRejectedValueOnce(new StorageQuotaError());
@@ -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 * Bridges the pure SnippetStore to the IndexedDB adapter via startup
* subscribers the store stays browser-free, and all reads/writes funnel * 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. */ /** Wire all persistence subscribers. Returns a teardown that detaches them. */
export function wirePersistence(): Unsubscribe { export function wireSnippetPersistence(): Unsubscribe {
const unsubs = [wireDraftAutoSave(), wireWriteThrough()]; const unsubs = [wireDraftAutoSave(), wireWriteThrough()];
return () => unsubs.forEach((u) => u()); return () => unsubs.forEach((u) => u());
} }
+2 -2
View File
@@ -19,7 +19,7 @@ import { notify } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore'; import { useSnippetStore } from '../stores/SnippetStore';
import { useDatasetStore } from '../stores/DatasetStore'; import { useDatasetStore } from '../stores/DatasetStore';
import { useCustomThemeStore } from '../stores/CustomThemeStore'; import { useCustomThemeStore } from '../stores/CustomThemeStore';
import { wirePersistence } from './persistence'; import { wireSnippetPersistence } from './snippet-persistence';
import { wireDatasetPersistence } from './dataset-persistence'; import { wireDatasetPersistence } from './dataset-persistence';
import { wireThemePersistence } from './theme-persistence'; import { wireThemePersistence } from './theme-persistence';
import { startRouting } from '../modals/UrlStateSync'; import { startRouting } from '../modals/UrlStateSync';
@@ -67,7 +67,7 @@ export async function initApp(): Promise<void> {
// Wire persistence AFTER hydrate so write-through's baseline is the loaded set // Wire persistence AFTER hydrate so write-through's baseline is the loaded set
// — otherwise it would redundantly re-save every record on each startup. // — otherwise it would redundantly re-save every record on each startup.
wirePersistence(); wireSnippetPersistence();
wireDatasetPersistence(); wireDatasetPersistence();
wireThemePersistence(); wireThemePersistence();
@@ -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 });
});
});
-54
View File
@@ -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) };
}
+32
View File
@@ -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', () => { describe('commitUrlSnapshot — create from a fetched body', () => {
test('snapshots the body, infers format from content, and profiles it', () => { test('snapshots the body, infers format from content, and profiles it', () => {
store().startCreate(); store().startCreate();
+3 -3
View File
@@ -5,9 +5,9 @@
* active, and the live editor buffer (`draftText`). Actions are the single place * 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. * snippet state mutates, so they are unit-testable without a DOM or IndexedDB.
* *
* Persistence is NOT done here a startup subscriber (orchestration/persistence) * Persistence is NOT done here a startup subscriber
* observes this store and writes through to the IndexedDB adapter. That keeps the * (orchestration/snippet-persistence) observes this store and writes through to
* store pure and free of browser APIs. * the IndexedDB adapter. That keeps the store pure and free of browser APIs.
* *
* Draft/published model (spec §03D): every snippet carries a published `spec` * Draft/published model (spec §03D): every snippet carries a published `spec`
* and a working `draftSpec`. Ordinary editing keystrokes, auto-save touches * and a working `draftSpec`. Ordinary editing keystrokes, auto-save touches
+2 -2
View File
@@ -6,8 +6,8 @@
* scan, never stored, so it can't drift. This module is that scan's single pure * 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 * 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 * data, so the same logic serves both the reactive UI (the Dataset Manager passes
* its live `snippets` selection) and the non-reactive `RelationshipService` (which * its live `snippets` selection) and any non-reactive caller (which passes a
* passes a store snapshot). One definition, per arch/07 §6 ("'who references this' * store snapshot inline). One definition, per arch/07 §6 ("'who references this'
* has exactly one implementation"). * has exactly one implementation").
* *
* Matching is **case-insensitive**, mirroring the naming policy (`core/naming`): * Matching is **case-insensitive**, mirroring the naming policy (`core/naming`):