mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Track datasetRefs against the live draft so manual edits and extract link immediately
This commit is contained in:
@@ -125,9 +125,15 @@ export function makeUniqueName(desired: string, existingNames: Iterable<string>)
|
||||
it by scanning snippets. Keeping it _derived_ means it can never disagree with
|
||||
the forward links — there is one source of truth.
|
||||
|
||||
`datasetRefs` is **derived from the spec**, not hand-maintained. It is
|
||||
recomputed whenever a snippet is published (its draft spec is promoted), so it
|
||||
always mirrors the dataset names actually referenced in the published spec.
|
||||
`datasetRefs` is **derived from the spec**, not hand-maintained. It mirrors the
|
||||
dataset names referenced by the **draft** spec — the version being edited — and
|
||||
is recomputed on every change to the draft (auto-save, the Extract-to-Dataset
|
||||
rewrite, revert) and on publish. Tracking the draft (not only the last publish)
|
||||
keeps a snippet's linked-datasets display and the reverse lookup in step with
|
||||
what the editor shows — so a hand-typed reference or an Extract links its dataset
|
||||
without waiting for a publish. Recomputation runs only on a _valid_ spec —
|
||||
auto-save is debounced and parse-gated (spec §03B) — so a transiently-invalid
|
||||
draft never disturbs the links.
|
||||
|
||||
### 3.1 Extracting referenced names from a spec (pure — `src/core/spec-refs.ts`)
|
||||
|
||||
@@ -186,7 +192,7 @@ function safeParse(s: string): Json {
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/core/spec-refs.ts — thin wrapper used at publish time
|
||||
// src/core/spec-refs.ts — thin wrapper, run on every draft change and on publish
|
||||
|
||||
/** The list stored on snippet.datasetRefs. Sorted + de-duped for stable diffs. */
|
||||
export function recomputeDatasetRefs(spec: Json): string[] {
|
||||
@@ -202,8 +208,11 @@ export function recomputeDatasetRefs(spec: Json): string[] {
|
||||
- Treat `extractDatasetRefs` as the single source of truth for "what does this
|
||||
spec reference". The reverse-lookup and rename paths both depend on it
|
||||
agreeing with what the renderer actually resolves.
|
||||
- Recompute and store `datasetRefs` on **publish**, not on every keystroke —
|
||||
the draft can be transiently invalid, and only the published spec is shared.
|
||||
- Recompute and store `datasetRefs` on **every draft change and on publish** —
|
||||
but only through the parse-gated, debounced auto-save (`commitDraft`) and the
|
||||
programmatic extract/revert rewrites, never on raw keystrokes. That keeps the
|
||||
links in step with the edited draft while never recomputing from a
|
||||
transiently-invalid spec.
|
||||
- Prune the **same two keys** (`data`, `datasets`) in all three ref walks —
|
||||
extraction here, `renameDatasetInSpec`, and the renderer's `resolveDatasetRefs`
|
||||
(`src/core/rendering.ts`). They must agree on what counts as a reference; if one
|
||||
@@ -264,8 +273,9 @@ export function findSnippetsReferencingDataset(name: string): Snippet[] {
|
||||
|
||||
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 is published with changed refs — no duplicated
|
||||
matching logic, no `getState()` snapshot that would miss updates.
|
||||
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**
|
||||
|
||||
@@ -375,8 +385,9 @@ one atomic step:
|
||||
2. For **every snippet referencing the old name**: rewrite the named-data
|
||||
references inside its spec (`{ "data": { "name": "old" } }` →
|
||||
`{ "data": { "name": "new" } }`) — in **both** `spec` and `draftSpec`.
|
||||
3. Recompute that snippet's `datasetRefs` from the rewritten spec, so the
|
||||
forward link mirrors reality and the reverse scan stays correct.
|
||||
3. Recompute that snippet's `datasetRefs` from the rewritten **draft** spec (the
|
||||
tracked surface), so the forward link mirrors reality and the reverse scan
|
||||
stays correct.
|
||||
|
||||
The spec rewrite is pure; the orchestration reads and writes stores.
|
||||
|
||||
@@ -417,42 +428,34 @@ export function renameDatasetInSpec(spec: Json, oldName: string, newName: string
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/app/services/RelationshipService.ts — store coordination
|
||||
// src/app/services/RelationshipService.ts — thin store coordinator
|
||||
|
||||
import { isNameTaken, makeUniqueName } from '../../core/naming';
|
||||
import { renameDatasetInSpec, recomputeDatasetRefs } from '../../core/spec-refs';
|
||||
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 snippets that changed.
|
||||
* (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): { updated: number } {
|
||||
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) return { updated: 0 };
|
||||
useDatasetStore.getState().update(dataset.id, { name: newName });
|
||||
if (dataset) useDatasetStore.getState().update(dataset.id, { name: newName }, now);
|
||||
|
||||
// 2 + 3. Rewrite every referencing snippet's specs and refs.
|
||||
let updated = 0;
|
||||
for (const snippet of useSnippetStore.getState().snippets) {
|
||||
if (!snippet.datasetRefs.some((r) => r.toLowerCase() === oldName.toLowerCase())) continue;
|
||||
|
||||
const spec = renameDatasetInSpec(snippet.spec, oldName, newName);
|
||||
const draftSpec = renameDatasetInSpec(snippet.draftSpec, oldName, newName);
|
||||
useSnippetStore.getState().update(snippet.id, {
|
||||
spec,
|
||||
draftSpec,
|
||||
datasetRefs: recomputeDatasetRefs(spec),
|
||||
});
|
||||
updated++;
|
||||
}
|
||||
|
||||
return { updated };
|
||||
// 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) };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -466,10 +469,17 @@ export function renameDatasetEverywhere(oldName: string, newName: string): { upd
|
||||
|
||||
- Rewrite `spec` **and** `draftSpec`. A user mid-edit must not see their draft
|
||||
silently break because the dataset was renamed underneath them.
|
||||
- Recompute `datasetRefs` from the rewritten spec rather than string-replacing
|
||||
the array — the spec is the source of truth, the array is its mirror.
|
||||
- Use the §4 reverse lookup to find affected snippets, so "who references this"
|
||||
has exactly one implementation.
|
||||
- Recompute `datasetRefs` from the rewritten **draft** spec rather than
|
||||
string-replacing the array — the draft is the source of truth, the array is
|
||||
its mirror.
|
||||
- Find affected snippets by scanning each one's **spec and draft content** for
|
||||
the old name (`extractDatasetRefs`), not by its `datasetRefs` array. Because
|
||||
`datasetRefs` mirrors the draft, a name referenced only by the still-published
|
||||
spec (the user removed it from the draft but hasn't published) is absent from
|
||||
the array; matching on content rewrites it anyway, so the published spec can't
|
||||
rot to a renamed-away dataset. Scan the content (don't reserialize) so a
|
||||
non-referencing snippet's text stays byte-for-byte intact. The import-side
|
||||
`applyDatasetRenamesToSnippets` already follows this spec-content rule (§5.1).
|
||||
|
||||
**Don't**
|
||||
|
||||
@@ -494,6 +504,6 @@ export function renameDatasetEverywhere(oldName: string, newName: string): { upd
|
||||
|
||||
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 rename rule of thumb — _the spec is the source of
|
||||
is an **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.
|
||||
|
||||
@@ -45,7 +45,7 @@ Every snippet carries two versions of its spec: a **published** (stable) version
|
||||
|
||||
- A **Publish** action promotes the current draft to become the published version (the two are made identical).
|
||||
- Publish is also triggered by the keyboard shortcut Cmd/Ctrl+S.
|
||||
- On publish, the snippet's dataset references are recomputed from the now-published spec (see _Datasets_ for reference linking).
|
||||
- The snippet's dataset references track the draft continuously (recomputed on auto-save, extract, and revert), so publish needs no special reference handling — promoting the draft simply carries the already-current references onto the published version (see _Datasets_ for reference linking).
|
||||
- A success toast confirms the snippet was published.
|
||||
- Publish is unavailable when no snippet is active.
|
||||
|
||||
@@ -72,7 +72,7 @@ When a snippet's spec embeds its data inline, the user can lift that data out in
|
||||
- When the active snippet's draft spec contains inline data, an **Extract to Dataset** action is available in the pane header; it is hidden when the spec has no inline data.
|
||||
- Choosing it opens a modal that shows a read-only preview of the inline data and asks the user for a dataset name (required).
|
||||
- The user enters a name and confirms creation. Names must be non-empty and unique; if the name is blank or already in use, the modal shows an inline error and the action does not proceed.
|
||||
- On success, the system: saves the inline data as a new dataset (preserving its detected format), rewrites the snippet's draft spec so the inline data is replaced by a reference to the dataset by name, links the dataset to the snippet, and reloads the editor to show the rewritten spec.
|
||||
- On success, the system: saves the inline data as a new dataset (preserving its detected format), rewrites the snippet's draft spec so the inline data is replaced by a reference to the dataset by name, links the dataset to the snippet **immediately** (the link appears without waiting for a publish, since references track the draft), and reloads the editor to show the rewritten spec. Reverting the draft before publishing removes the link again.
|
||||
- A toast confirms the dataset was created, and the modal closes.
|
||||
- The user can cancel the modal at any time, leaving the spec unchanged.
|
||||
- Dataset-side specifics (formats, storage, the bidirectional snippet↔dataset link) are described in _Datasets_.
|
||||
|
||||
@@ -28,7 +28,7 @@ A snippet carries two specs at once. `draftSpec` is the editable working copy; `
|
||||
|
||||
### datasetRefs
|
||||
|
||||
`datasetRefs` records the **names** of datasets the spec depends on. It is the link used to display a snippet's linked datasets and, conversely, to find which snippets use a given dataset (see _Cross-entity relationships_). It is maintained to mirror the dataset names actually referenced in the spec.
|
||||
`datasetRefs` records the **names** of datasets the spec depends on. It is the link used to display a snippet's linked datasets and, conversely, to find which snippets use a given dataset (see _Cross-entity relationships_). It mirrors the dataset names referenced by the **draft** spec — the version the user is editing — and is recomputed on every change to the draft (auto-save, the Extract-to-Dataset rewrite, revert) and on publish. Tracking the draft means a snippet's linked datasets reflect what the editor currently shows, not only the last published version; recomputation only ever runs on a valid (parseable) spec, so a half-typed draft never disturbs the links.
|
||||
|
||||
## B. Dataset
|
||||
|
||||
|
||||
Reference in New Issue
Block a user