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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Keep Codebase Evolution strict when a stored revision uses an unsupported
artifact layout, and offer to rebuild the affected revision with the current
Compass version instead of mapping legacy history records.

- Make community detail graphs easier to scan in both exported HTML and VS
Code by grouping node kinds into accessible color-and-shape families,
coloring edges by relationship purpose while retaining confidence strokes,
Expand Down
4 changes: 2 additions & 2 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ Versioned history remains on realization schema 1, store-format root
`compass/store-format/v1`, and the `compass/v1` realization-root namespace.
The visible output-path cutover does not change those serialized contracts.
Historical realizations containing former hidden artifact paths are not mapped
or rewritten; rebuild those revisions when they must be materialized with the
current visible artifact layout.
or rewritten; run `compass history rebuild REV` for a revision that must use
the current visible artifact layout.

The current local build publishes `graph.json` (`compass.graph/1`) directly
under the selected output root by default. It also materializes
Expand Down
6 changes: 3 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ compass update .

The history realization schema and SQLite store format remain at v1. Compass
does not rewrite immutable realizations or map former hidden artifact paths.
Use `compass history build` to recreate any revision that must materialize with
the current visible artifact layout; archive the existing history database
first only when it is needed for audit or rollback.
Use `compass history rebuild` to recreate any revision that must materialize
with the current visible artifact layout; archive the existing history
database first only when it is needed for audit or rollback.

## Compass Store sidecar upgrades

Expand Down
27 changes: 24 additions & 3 deletions crates/compass-history/src/graph_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ impl RealizationReader<'_> {
"labels.json" => sink.labels(value)?,
"analysis.json" => {}
_ => {
return Err(HistoryError::InvalidArtifacts(format!(
"unknown analysis sidecar {path}"
)));
return Err(unsupported_analysis_sidecar(
&self.published.version.git_commit,
&path,
));
}
}
}
Expand Down Expand Up @@ -196,3 +197,23 @@ fn text(bytes: &[u8], kind: &str) -> Result<String, HistoryError> {
String::from_utf8(bytes.to_vec())
.map_err(|error| HistoryError::InvalidArtifacts(format!("non-UTF-8 {kind}: {error}")))
}

fn unsupported_analysis_sidecar(revision: &str, path: &str) -> HistoryError {
HistoryError::InvalidArtifacts(format!(
"stored graph for revision {revision} uses an unsupported artifact layout ({path}); rebuild this revision graph with the current Compass version"
))
}

#[cfg(test)]
mod tests {
use super::unsupported_analysis_sidecar;

#[test]
fn unsupported_sidecars_request_a_current_graph_rebuild() {
let error = unsupported_analysis_sidecar("abcdef123456", ".compass_analysis.json");
assert_eq!(
error.to_string(),
"invalid graph artifacts: stored graph for revision abcdef123456 uses an unsupported artifact layout (.compass_analysis.json); rebuild this revision graph with the current Compass version"
);
}
}
12 changes: 12 additions & 0 deletions editors/vscode/src/history/buildArguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ describe("history build arguments", () => {
"history", "enable"
]);
});

it("uses explicit rebuild semantics for an obsolete stored graph", () => {
expect(buildHistoryArgs({
revision: "abc",
all: false,
firstParent: false,
rebuild: true,
profile: { kind: "configured" }
})).toEqual([
"history", "rebuild", "abc", "--format", "json", "--events", "jsonl"
]);
});
});
37 changes: 37 additions & 0 deletions editors/vscode/src/history/rebuildRecovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import {
RevisionGraphRebuildRequired,
rebuildRequiredMessage,
withRevisionGraphContext
} from "./rebuildRecovery";

describe("history graph rebuild recovery", () => {
it("identifies the affected revision from a strict history reader failure", async () => {
const detail = "invalid graph artifacts: stored graph for revision abc uses an unsupported artifact layout (old.json); rebuild this revision graph with the current Compass version";
const result = withRevisionGraphContext("abcdef123456", async () => {
throw new Error(detail);
});

await expect(result).rejects.toMatchObject({
name: "RevisionGraphRebuildRequired",
commit: "abcdef123456",
detail
});
await expect(result).rejects.toBeInstanceOf(RevisionGraphRebuildRequired);
});

it("preserves unrelated failures", async () => {
const original = new Error("history database is unavailable");
const result = withRevisionGraphContext("abcdef123456", async () => {
throw original;
});

await expect(result).rejects.toBe(original);
});

it("uses concise, actionable copy", () => {
expect(rebuildRequiredMessage("abcdef123456")).toBe(
"The stored graph for abcdef123 uses an unsupported format. Rebuild it with the current Compass version, then try again."
);
});
});
31 changes: 31 additions & 0 deletions editors/vscode/src/history/rebuildRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const REBUILD_REQUIRED_MARKER =
"rebuild this revision graph with the current Compass version";

export class RevisionGraphRebuildRequired extends Error {
constructor(
public readonly commit: string,
public readonly detail: string
) {
super(rebuildRequiredMessage(commit));
this.name = "RevisionGraphRebuildRequired";
}
}

export async function withRevisionGraphContext<T>(
commit: string,
load: () => Promise<T>
): Promise<T> {
try {
return await load();
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
if (detail.includes(REBUILD_REQUIRED_MARKER)) {
throw new RevisionGraphRebuildRequired(commit, detail);
}
throw error;
}
}

export function rebuildRequiredMessage(commit: string): string {
return `The stored graph for ${commit.slice(0, 9)} uses an unsupported format. Rebuild it with the current Compass version, then try again.`;
}
86 changes: 65 additions & 21 deletions editors/vscode/src/views/historyPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { HistoryTimeline } from "@compass/viewer";
import { buildEnableHistoryArgs, buildHistoryArgs } from "../history/buildArguments";
import { loadSemanticDiff } from "../history/diffClient";
import { loadChangeCounts } from "../history/changeCountsClient";
import {
RevisionGraphRebuildRequired,
withRevisionGraphContext
} from "../history/rebuildRecovery";
import { RevisionStore } from "../history/revisionStore";
import { loadTimeline } from "../history/timelineClient";
import {
Expand Down Expand Up @@ -169,7 +173,7 @@ export async function openHistoryPanel(
activePanelBuild.command.cancel();
}
});
const buildRevision = async (commit: string): Promise<void> => {
const buildRevision = async (commit: string, rebuild = false): Promise<void> => {
if (disposed) return;
if (session.activeWriter) {
await postMessage({
Expand Down Expand Up @@ -197,7 +201,7 @@ export async function openHistoryPanel(
value: { kind: "from" as const }
}
],
{ title: `Build graph for ${commit.slice(0, 9)}` }
{ title: `${rebuild ? "Rebuild" : "Build"} graph for ${commit.slice(0, 9)}` }
);
if (disposed) return;
if (!profile) {
Expand Down Expand Up @@ -235,6 +239,7 @@ export async function openHistoryPanel(
revision: commit,
all: false,
firstParent: false,
rebuild,
profile: selectedProfile
}),
(event) => {
Expand All @@ -250,7 +255,7 @@ export async function openHistoryPanel(
const result = await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: `Building Compass graph for ${commit.slice(0, 9)}`,
title: `${rebuild ? "Rebuilding" : "Building"} Compass graph for ${commit.slice(0, 9)}`,
cancellable: true
},
async (progress, token) => {
Expand Down Expand Up @@ -407,10 +412,13 @@ export async function openHistoryPanel(
const generation = ++viewGeneration;
activeComparison = undefined;
activeSourceCommits = new Set();
const revision = await revisions.load(
const revision = await withRevisionGraphContext(
message.commit,
graphNodeLimit,
historyIdentity(entry)
() => revisions.load(
message.commit,
graphNodeLimit,
historyIdentity(entry)
)
);
if (generation !== viewGeneration) return;
activeSourceCommits = new Set([message.commit]);
Expand Down Expand Up @@ -438,11 +446,14 @@ export async function openHistoryPanel(
realization: message.realization,
fingerprint: message.fingerprint
};
const revision = await revisions.loadCommunity(
const revision = await withRevisionGraphContext(
message.commit,
message.communityId,
graphNodeLimit,
expected
() => revisions.loadCommunity(
message.commit,
message.communityId,
graphNodeLimit,
expected
)
);
await postMessage({
type: "communityGraph",
Expand All @@ -464,9 +475,17 @@ export async function openHistoryPanel(
const generation = ++viewGeneration;
activeComparison = undefined;
activeSourceCommits = new Set([message.commit]);
const [current, parent, semanticDiff, counts] = await Promise.all([
revisions.load(message.commit, graphNodeLimit, historyIdentity(currentEntry)),
revisions.load(message.parent, graphNodeLimit, historyIdentity(parentEntry)),
const [current, parent] = await Promise.all([
withRevisionGraphContext(
message.commit,
() => revisions.load(message.commit, graphNodeLimit, historyIdentity(currentEntry))
),
withRevisionGraphContext(
message.parent,
() => revisions.load(message.parent, graphNodeLimit, historyIdentity(parentEntry))
)
]);
const [semanticDiff, counts] = await Promise.all([
loadSemanticDiff(session, message.parent, message.commit),
loadChangeCounts(session, message.commit, message.parent)
]);
Expand Down Expand Up @@ -536,19 +555,25 @@ export async function openHistoryPanel(
try {
[current, parent] = await Promise.all([
message.hasCurrent
? revisions.loadCommunity(
? withRevisionGraphContext(
message.commit,
message.communityId,
graphNodeLimit,
comparisonState.currentIdentity
() => revisions.loadCommunity(
message.commit,
message.communityId,
graphNodeLimit,
comparisonState.currentIdentity
)
)
: undefined,
message.hasParent
? revisions.loadCommunity(
? withRevisionGraphContext(
message.parent,
message.communityId,
graphNodeLimit,
comparisonState.parentIdentity
() => revisions.loadCommunity(
message.parent,
message.communityId,
graphNodeLimit,
comparisonState.parentIdentity
)
)
: undefined
]);
Expand Down Expand Up @@ -593,6 +618,25 @@ export async function openHistoryPanel(
);
}
} catch (error) {
if (error instanceof RevisionGraphRebuildRequired) {
output.appendLine(`[history:error] ${error.detail}`);
const action = await vscode.window.showWarningMessage(
error.message,
"Rebuild graph"
);
if (action === "Rebuild graph") {
await buildRevision(error.commit, true);
} else {
const commit = typeof message?.commit === "string" ? message.commit : error.commit;
await postMessage({
type: "error",
operation: historyOperationFor(message),
commit,
message: error.message
});
}
return;
}
if (message?.type === "buildRevision" && typeof message.commit === "string") {
const fullMessage = error instanceof Error ? error.message : String(error);
output.appendLine(`[history:error] ${fullMessage}`);
Expand Down
Loading