Fix dataset-ref walk false positive and wire spec-mandated success toasts

- Prune the data payload and top-level datasets keys in all three ref walks
  (extractDatasetRefs, renameDatasetInSpec, resolveDatasetRefs) so a data row
  carrying a field named "data" is no longer misread as a library reference,
  spuriously rewritten, or made to throw DatasetNotFoundError. Adds tests,
  including a guard that lookup-transform refs (from.data) still resolve.
- Wire the deferred success toasts now the Toaster has landed: publish, revert,
  extract-to-dataset, and snippet/dataset delete. Copy follows the council
  title-vs-message rule (title states the action, message adds the consequence).
- Reconcile the spec's blanket toast mandate to "toast only what the user can't
  already see": no toast on visible-result creates (snippet, dataset form);
  Copy Reference stays inline and gains an aria-live announcement (new shared
  .visually-hidden utility) instead of a toast-per-copy.
- Move the toast region to bottom-right so it stops covering the header action
  cluster (Publish/Revert, theme/datasets).
- Update docs/spec 01F/02/05 and docs/architecture/07 + 10 to match.
This commit is contained in:
2026-06-05 16:34:58 +03:00
parent a4e4d96d3b
commit 693f5d7073
17 changed files with 263 additions and 67 deletions
+16 -14
View File
@@ -149,11 +149,12 @@ function resolvedData(dataset: ResolvableDataset, rest: SpecNode): SpecNode {
/**
* Replace every named-data reference in `node` with its library dataset's
* contents, recursing through the entire spec (arrays and objects) so refs
* anywhere are resolved — matching `extractDatasetRefs`. A self-defined name is
* left untouched; an unknown library name throws `DatasetNotFoundError`. Matching
* is case-insensitive, mirroring naming.ts. Mutates in place; the caller already
* works on a copy.
* contents, recursing through the spec (arrays and objects) so refs anywhere are
* resolved — matching `extractDatasetRefs`, including pruning the `data` and
* top-level `datasets` payload keys so resolution never descends into user data
* rows. A self-defined name is left untouched; an unknown library name throws
* `DatasetNotFoundError`. Matching is case-insensitive, mirroring naming.ts.
* Mutates in place; the caller already works on a copy.
*/
function resolveDatasetRefs(
node: unknown,
@@ -177,15 +178,16 @@ function resolveDatasetRefs(
}
}
// TODO: this walks EVERY key, so it also descends into inlined data payloads
// (the just-resolved `values`, a spec's `datasets`/`data.values`). That's
// wasteful for large inline data on every debounced render, and a row with a
// field literally named `data` holding `{ name: "x" }` would be spuriously
// resolved or throw DatasetNotFoundError. extractDatasetRefs shares this broad
// walk. A scoped walk (recurse only into the known sub-spec/container keys +
// `transform[].lookup.from`, never into data payloads) would be safer and
// faster — change deliberately, with tests for where refs may legally appear.
for (const key of Object.keys(node)) resolveDatasetRefs(node[key], byName, selfDefined);
// Recurse into every key except the two that hold data payloads (`data` —
// resolved/captured above; `datasets` — the spec's own inline data). Pruning
// them avoids descending into the just-resolved `values` and into user data
// rows, where a field named `data` holding `{ name: "x" }` would otherwise be
// spuriously resolved or throw DatasetNotFoundError. extractDatasetRefs prunes
// the same two keys so resolution and extraction stay in agreement.
for (const key of Object.keys(node)) {
if (key === 'data' || key === 'datasets') continue;
resolveDatasetRefs(node[key], byName, selfDefined);
}
}
/**