Add dataset library, extract-to-dataset, and render-time reference resolution

This commit is contained in:
2026-06-05 15:49:40 +03:00
parent 25849461e0
commit a4e4d96d3b
41 changed files with 3909 additions and 19 deletions
+76 -2
View File
@@ -19,6 +19,7 @@
import { create } from 'zustand';
import { createSnippet, type CreateSnippetOptions, type Snippet } from '@core/snippet';
import { recomputeDatasetRefs, renameDatasetInSpec } from '@core/spec-refs';
/** Which version of the active snippet the editor is showing (spec §03D). */
export type EditorView = 'draft' | 'published';
@@ -48,6 +49,13 @@ export interface SnippetState {
removeSnippet: (id: string) => void;
/** Update the draft buffer only (no persistence; debounced commit follows). */
updateDraft: (text: string) => void;
/**
* Replace the active snippet's draft spec with new text and reload the editor
* on the draft view (bumps `bufferEpoch`). Used by programmatic rewrites such
* as Extract-to-Dataset (spec §03F), which substitutes inline data for a
* by-name reference. No-op when no snippet is active. `now` injectable.
*/
replaceActiveDraft: (text: string, now?: Date) => void;
/**
* Persist the editor buffer into the active snippet's **draft**, if it parses
* as JSON. Returns whether it committed (a half-typed, unparseable buffer is
@@ -57,6 +65,14 @@ export interface SnippetState {
commitDraft: (now?: Date) => boolean;
/** Switch the editor between the draft and published views (spec §03D). */
setEditorView: (view: EditorView) => void;
/**
* Propagate a dataset rename across every referencing snippet: rewrite the
* named-data references in both `spec` and `draftSpec`, recompute `datasetRefs`,
* and refresh the live editor buffer if the active snippet was rewritten — so a
* user mid-edit doesn't see their draft silently break (docs/architecture/07
* §6). Returns the number of snippets changed. `now` injectable.
*/
renameDatasetRefs: (oldName: string, newName: string, now?: Date) => number;
/**
* Promote the active snippet's current draft to its published version (spec
* §03D → Publish). Flushes the live buffer first, then makes `spec` identical
@@ -151,6 +167,20 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
updateDraft: (draftText) => set({ draftText }),
replaceActiveDraft: (text, now) => {
const { activeSnippetId } = get();
if (!activeSnippetId) return;
const modified = (now ?? new Date()).toISOString();
set((s) => ({
snippets: s.snippets.map((x) =>
x.id === activeSnippetId ? { ...x, draftSpec: text, modified } : x,
),
draftText: text,
editorView: 'draft',
bufferEpoch: s.bufferEpoch + 1,
}));
},
commitDraft: (now) => {
const { activeSnippetId, draftText, snippets } = get();
if (!activeSnippetId) return false;
@@ -178,6 +208,43 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
setEditorView: (editorView) => set({ editorView }),
renameDatasetRefs: (oldName, newName, now) => {
if (oldName === newName) return 0;
const lower = oldName.toLowerCase();
const { snippets, activeSnippetId, editorView } = get();
let updated = 0;
let activeDraftAfter: string | null = null;
const next = snippets.map((s) => {
if (!s.datasetRefs.some((r) => r.toLowerCase() === lower)) return s;
updated++;
const spec = renameDatasetInSpec(s.spec, oldName, newName);
const draftSpec = renameDatasetInSpec(s.draftSpec, oldName, newName);
if (s.id === activeSnippetId) activeDraftAfter = draftSpec;
return {
...s,
spec,
draftSpec,
datasetRefs: recomputeDatasetRefs(spec),
modified: (now ?? new Date()).toISOString(),
};
});
if (updated === 0) return 0;
set((st) => ({
snippets: next,
// If the active snippet's draft was rewritten, reload the editor buffer so
// the live (draft-view) buffer reflects the new name and the next auto-save
// doesn't clobber the rewrite with the stale old name.
...(activeDraftAfter !== null && editorView === 'draft'
? { draftText: activeDraftAfter, bufferEpoch: st.bufferEpoch + 1 }
: activeDraftAfter !== null
? { bufferEpoch: st.bufferEpoch + 1 }
: {}),
}));
return updated;
},
publish: (now) => {
// Flush the live buffer into the draft first, so Publish promotes exactly
// what the user sees (an invalid buffer leaves the last valid draft in place).
@@ -189,8 +256,15 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
set({
snippets: snippets.map((s) =>
s.id === activeSnippetId
? // M3: recompute datasetRefs from the now-published spec (spec §03D).
{ ...s, spec: s.draftSpec, modified }
? // Promote the draft and recompute datasetRefs from the now-published
// spec, so the bidirectional snippet↔dataset link mirrors reality
// (spec §03D, docs/architecture/07 §3).
{
...s,
spec: s.draftSpec,
datasetRefs: recomputeDatasetRefs(s.draftSpec),
modified,
}
: s,
),
});