Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Removed
- **The unused saved-query repair planner has been removed** (#429 phase 6 /
#500). Direct, ownership-safe Panel and Dashboard trash actions are the
supported cascading delete paths; Library deletion and saved-query edits
continue to fail closed when whole-workspace validation would invalidate a
live Dashboard member.

## [0.7.0] - 2026-07-27

### Added
Expand Down
6 changes: 3 additions & 3 deletions src/application/dashboard-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,9 @@ const variableAnnotation = (
*
* Both say what is wrong with the DATA rather than "not allowed": the row is
* showing a tile whose dedicated query cannot be proven, and the user's next
* move is to look at the panel, not to try again. Repairing such a workspace
* is #429's repair-planner phase, deliberately not something these controls
* attempt — a guessed owner is how one delete becomes two.
* move is to inspect the workspace data, not to try again. #429 deliberately
* keeps malformed ownership fail-closed rather than guessing a repair — a
* guessed owner is how one delete becomes two.
*/
const MISSING_PANEL_QUERY_REASON =
'This panel’s query is not in this workspace, so there is nothing to edit or remove.';
Expand Down
195 changes: 0 additions & 195 deletions src/dashboard/application/saved-query-mutation.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/dashboard/layouts/grafana-grid-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,9 @@ function tileRefsOf(tiles: readonly unknown[]): GrafanaGridFallbackTile[] {
* `layout.fallback`, mirroring `setGridPlacement`'s own mutate-in-place
* contract) — a no-op when `layout` is not a grafana-grid@1 document. The
* single shared primitive every #291 application-layer mutation path
* (authoring commands, tile-membership star toggle, saved-query mutation
* planning) calls so "every grid mutation regenerates the flow@1 fallback
* deterministically" is enforced once, not duplicated per call site. The
* (authoring commands and tile membership) calls so "every grid mutation
* regenerates the flow@1 fallback deterministically" is enforced once, not
* duplicated per call site. The
* non-grid guard runs BEFORE the tiles→refs mapping/allocation (#291 review
* F9) — calling this on the far-more-common flow-engine document costs only
* the guard check, never a `tiles[]` walk that would just be thrown away. */
Expand Down
4 changes: 2 additions & 2 deletions src/dashboard/layouts/layout-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ export function createLayoutRegistry(
export const defaultLayoutRegistry: DashboardLayoutRegistry =
createLayoutRegistry(BUILTIN_SYNC_PLUGINS.map(syncRegistration));

/** Synchronous plugin resolution for pure call sites that mutate a Dashboard
/** Synchronous plugin resolution for call sites that mutate a Dashboard
* document but cannot await the async registry (`tile-membership.ts`,
* `saved-query-mutation.ts` — #291): looked up in `BUILTIN_SYNC_PLUGINS` by
* `library-assignment.ts` — #291/#428): looked up in `BUILTIN_SYNC_PLUGINS` by
* exact `{type, version}` match, else the flow@1 plugin. Both built-in
* plugins are stateless, already-constructed values (`load()` never truly
* defers for either — see the module doc comment above), so no async is
Expand Down
4 changes: 2 additions & 2 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,8 +1361,8 @@ export async function deleteSaved(
id: string, mutate: MutateWorkspace,
): Promise<CommitOnlyResult> {
// Delete by ID from the LATEST workspace (#343): the whole-workspace
// validation/repair policy runs against every `latest` Dashboard (not a stale
// Workbench Dashboard snapshot) via the commit inside `mutateWorkspace`.
// fail-closed validation policy runs against every `latest` Dashboard (not a
// stale Workbench Dashboard snapshot) via the commit inside `mutateWorkspace`.
const outcome = await mutate((latest) => {
const base = baselineWorkspace(state, latest);
return { candidate: candidateFrom(base, base.queries.filter((q) => q.id !== id)) };
Expand Down
13 changes: 6 additions & 7 deletions src/workspace/import-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
// A PortableBundle import always resolves to one COMPLETE candidate
// StoredWorkspaceV5 built from the repository-level primitives in
// workspace-operations.ts, then validated in one pass through
// validateStoredWorkspaceDocument — exactly the same "build the whole
// candidate, validate once, never commit an invalid one" discipline
// saved-query-mutation.ts uses for in-place mutations. Nothing here mutates
// application state; the caller commits the returned candidate atomically
// through the Phase-2 repository, or does not commit at all.
// validateStoredWorkspaceDocument — the same "build the whole candidate,
// validate once, never commit an invalid one" discipline every strict
// workspace write uses. Nothing here mutates application state; the caller
// commits the returned candidate atomically through the Phase-2 repository,
// or does not commit at all.
//
// Query-identity conflicts are resolved BY ID, never by content-based dedup
// (#280): an incoming query conflicts with an existing one only when their
Expand Down Expand Up @@ -195,8 +195,7 @@ export interface RewriteDashboardReferencesResult {
}

/**
* Bulk generalization of `saved-query-mutation.ts`'s `remapQuery`: rewrite
* every `tile.queryId` and `filter.sourceQueryId` through `mapping`. A
* Rewrite every `tile.queryId` and `filter.sourceQueryId` through `mapping`. A
* reference that maps to `null` (skipped) or has no mapping entry at all
* sets `invalidated: true` and collects the source id in
* `missingRequiredIds` — the reference is left as-is (never silently
Expand Down
4 changes: 2 additions & 2 deletions src/workspace/stored-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ function structuralDiagnostics(

/** Complete deterministic validation of one V5 stored-workspace aggregate —
* the same pipeline `WorkspaceRepository.commit` runs before any write, and
* the pipeline every candidate builder (import planner, saved-query mutation
* planner) validates its candidate through. */
* the pipeline every candidate builder (imports, saved-query writes,
* Dashboard commands) validates its candidate through. */
export function validateStoredWorkspaceDocument(
document: unknown, { validationService = jsonSchemaValidationService }: WorkspaceCodecOptions = {},
): WorkspaceDiagnostic[] {
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/dashboard-boundaries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,23 @@ describe('dashboard dependency boundaries', () => {
it('src/workspace imports no Workbench UI / App / AppState / editor / service / net modules', () => {
expect(violations('src/workspace')).toEqual([]);
});

it('does not restore the retired saved-query repair planner or its vocabulary', () => {
const retiredPath = ['saved-query', 'mutation.ts'].join('-');
expect(existsSync(join(repoRoot, 'src/dashboard/application', retiredPath))).toBe(false);
const retiredTerms = [
['plan', 'SavedQuery', 'Mutation'].join(''),
['suggest', 'Repairs'].join(''),
['SavedQuery', 'Repair'].join(''),
['remove', '-affected', '-tiles'].join(''),
];
const hits = [];
for (const file of [...collectFiles(join(repoRoot, 'src')), ...collectFiles(join(repoRoot, 'tests'))]) {
const source = readFileSync(file, 'utf8');
for (const term of retiredTerms) {
if (source.includes(term)) hits.push(`${relative(repoRoot, file)} → ${term}`);
}
}
expect(hits).toEqual([]);
});
});
7 changes: 3 additions & 4 deletions tests/unit/grafana-grid-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,9 @@ describe('regenerateGridFallback', () => {
});

// #291 review F9: the id-extraction/filtering used to be built by every
// call site (dashboard-commands.ts, tile-membership.ts,
// saved-query-mutation.ts) before calling this function — now it accepts
// the RAW `dashboard.tiles[]`-shaped array directly and does its own
// filtering, tolerating a malformed entry.
// call site (dashboard-commands.ts and tile-membership.ts) before calling
// this function — now it accepts the RAW `dashboard.tiles[]`-shaped array
// directly and does its own filtering, tolerating a malformed entry.
it('accepts a raw dashboard.tiles[]-shaped array directly, dropping a malformed entry', () => {
const layout = gridLayout({ a: { span: 4 } });
regenerateGridFallback(layout, [
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/query-ownership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ describe('libraryQueries', () => {
);
expect(libraryQueries(ws)).toEqual([]);
});

it('excludes a query owned only by a non-compatibility Dashboard', () => {
const ws = workspace(
[query('a'), query('owned-elsewhere'), query('b')],
[dashboard('compatibility', []), dashboard('other', ['owned-elsewhere'])],
);
expect(libraryQueries(ws).map((entry) => entry.id)).toEqual(['a', 'b']);
});
});

describe('ownersOfQuery', () => {
Expand Down
Loading