From 532072cceb70e64078d05af10299f7b18a7ba279 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Wed, 15 Jul 2026 09:53:17 -0700 Subject: [PATCH] Prepare Tabellio 0.2.0 release Entire-Checkpoint: a23449f406cf --- CHANGELOG.md | 18 +- docs/evidence-schema.md | 2 +- docs/review-loop.md | 37 --- package.json | 2 +- scripts/lib/agent-run.mjs | 4 + scripts/lib/control-ref-transport.mjs | 1 + scripts/lib/git-json-ledger.mjs | 3 + scripts/lib/ledger-provider.mjs | 2 + scripts/lib/repository-store.mjs | 9 + scripts/lib/review-cycle.mjs | 243 +----------------- scripts/lib/stack-manager.mjs | 1 + scripts/providers/entire-ledger-provider.mjs | 2 + scripts/providers/git-spice-operations.mjs | 1 + scripts/providers/git-spice-stack-manager.mjs | 1 + scripts/providers/native-git-store.mjs | 10 + scripts/tabellio-review.mjs | 43 +--- tests/review-cycle.test.mjs | 207 --------------- 17 files changed, 57 insertions(+), 529 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77bd8b..c2b22a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to Tabellio are recorded here. ## Unreleased +## 0.2.0 - 2026-07-15 + ### Added - GitHub-bound exact-commit validation runner with committed argv manifests and no shell execution. @@ -11,7 +13,6 @@ All notable changes to Tabellio are recorded here. - Local validation results integrated into durable review readiness. - Git-native JSON ledger on compare-and-swap refs for durable control-plane state. - Durable review cycles covering GitHub feedback, agent findings, triage, checks, fixes, and readiness. -- Dry-run-first, atomic review-cycle migration from legacy v0.1 identities and PR numbers to GitHub-only v0.2 state. - Entire-checkpoint-bound fix records with commit remapping across git-spice restacks. - Approval-gated git-spice submit, update, sync, restack, and merge operations. - Integrity-bound operation intents, short-lived approvals, one-use receipts, and branch-set race checks. @@ -33,7 +34,8 @@ All notable changes to Tabellio are recorded here. ### Fixed -- Removed 15 unused public exports and consolidated provider subprocess handling; inherited Fallow dead-code findings dropped from 49 to 34 without deleting runtime interface methods. +- Removed 15 unused public exports and consolidated provider subprocess handling. +- Classified all 34 reported interface and injected class methods against runtime and test call sites, then added narrow stale-checked Fallow suppressions; unresolved dead-code findings are zero. - Evidence artifact self-integrity now has an explicit canonical hash scope. - Handwritten policy validators now require approval booleans, unique action IDs, and non-empty forbidden side effects. - Runtime context validation now matches schema property boundaries. @@ -51,6 +53,18 @@ All notable changes to Tabellio are recorded here. - Made GitHub `origin` the canonical code store and thin pull-request shell while keeping agent control state external. - Rejected publication or fetching of review, validation, and Entire control refs through the code-storage remote. +### Removed + +- Retired the completed one-time legacy review-cycle migration command, decoder, remap helpers, documentation, and fixtures. Recovery code remains available in Git history. + +### Release Gates + +- `npm run check` +- Fallow whole-repository dead-code and stale-suppression scan +- Fallow changed-code audit against `origin/main` +- `npm pack --dry-run --json` +- Exact-head Tabellio validation + ## 0.1.0 - 2026-07-08 Initial public release. diff --git a/docs/evidence-schema.md b/docs/evidence-schema.md index aa38d53..9c711d7 100644 --- a/docs/evidence-schema.md +++ b/docs/evidence-schema.md @@ -117,4 +117,4 @@ node scripts/write-tabellio-evidence-envelope.mjs \ ## Boundary -This schema is simpler than SLSA provenance or in-toto link metadata. Future versions can export to those formats, but v0.1.0 does not claim compliance. +This schema is simpler than SLSA provenance or in-toto link metadata. Future versions can export to those formats, but Tabellio does not claim compliance. diff --git a/docs/review-loop.md b/docs/review-loop.md index 2fe55fb..2f5fb5e 100644 --- a/docs/review-loop.md +++ b/docs/review-loop.md @@ -42,43 +42,6 @@ node scripts/tabellio-review.mjs sync \ Sync imports GitHub reviews, inline review comments, issue comments, commit statuses, check runs, and the newest Tabellio validation for the PR head. `GITHUB_TOKEN` may replace `--token-file`; `GITHUB_API_URL` or `--api-url` may target GitHub Enterprise Server. Missing GitHub items are retained as stale evidence rather than silently deleted. A PR with no validation remains `validating`. -## Migrate A v0.1 Cycle - -Review cycles created before the GitHub-only boundary use `tabellio-review-cycle/v0.1` and a legacy ledger path. Preview the deterministic migration first: - -```bash -node scripts/tabellio-review.mjs migrate \ - --repo . \ - --repo-id IntelIP/Tabellio \ - --owner IntelIP \ - --remote-repo Tabellio \ - --number 7 \ - --target-number 14 \ - --legacy-repo-id old-owner/old-repository \ - --legacy-owner old-owner \ - --legacy-remote-repo old-repository -``` - -Apply the verified plan explicitly: - -```bash -node scripts/tabellio-review.mjs migrate \ - --repo . \ - --repo-id IntelIP/Tabellio \ - --owner IntelIP \ - --remote-repo Tabellio \ - --number 7 \ - --target-number 14 \ - --legacy-repo-id old-owner/old-repository \ - --legacy-owner old-owner \ - --legacy-remote-repo old-repository \ - --apply true -``` - -`--number` identifies the legacy cycle. `--target-number` identifies the corresponding GitHub pull request and defaults to `--number`. Legacy identity options default to the target coordinates, so they are needed only when the repository moved or was renamed. The migration verifies the v0.1 integrity digest and source identity, maps repository and provider identity to the GitHub target, rewrites feedback sources, replaces the pull-request URL with its canonical GitHub URL, clears obsolete provider check links, recomputes integrity, and atomically moves the ledger entry to its v0.2 path. It can also atomically correct a previously migrated cycle whose GitHub PR number was mapped incorrectly; that recovery path additionally requires `--remap-current true`. The current ledger tree contains only the v0.2 entry; prior forms remain recoverable from Git history. Repeating the command reports `current` without writing another commit. - -Migration marks the provider-owned change-request ID as pending. Run `sync` for the target GitHub PR immediately after applying so GitHub refreshes the ID, title, state, head/base commits, feedback, and checks. - ## Import A Codex Review Codex and other agents emit `tabellio-agent-review/v0.1`: diff --git a/package.json b/package.json index 99a7a86..ef1331a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@intelip/tabellio", - "version": "0.1.0", + "version": "0.2.0", "description": "Tabellio: GitHub-native context and evidence for agentic development.", "type": "module", "license": "Apache-2.0", diff --git a/scripts/lib/agent-run.mjs b/scripts/lib/agent-run.mjs index 6089211..78725cf 100644 --- a/scripts/lib/agent-run.mjs +++ b/scripts/lib/agent-run.mjs @@ -94,6 +94,7 @@ export class AgentRunManager { }); } + // fallow-ignore-next-line unused-class-member async checkpoint({ runId, summary }) { return this.#withLock(runId, async () => { const state = await this.#readState(runId); @@ -132,6 +133,7 @@ export class AgentRunManager { }); } + // fallow-ignore-next-line unused-class-member async finish({ runId, validationCommand, onValidationOutput = null }) { return this.#withLock(runId, async () => { const state = await this.#readState(runId); @@ -221,6 +223,7 @@ export class AgentRunManager { }); } + // fallow-ignore-next-line unused-class-member async promote({ runId }) { return this.#withLock(runId, async () => { const state = await this.#readState(runId); @@ -247,6 +250,7 @@ export class AgentRunManager { }); } + // fallow-ignore-next-line unused-class-member async status({ runId }) { return this.#result(await this.#readState(runId)); } diff --git a/scripts/lib/control-ref-transport.mjs b/scripts/lib/control-ref-transport.mjs index eb58388..42f4203 100644 --- a/scripts/lib/control-ref-transport.mjs +++ b/scripts/lib/control-ref-transport.mjs @@ -110,6 +110,7 @@ export class ApprovedControlRefTransport { }); } + // fallow-ignore-next-line unused-class-member async execute({ intent, approval, repositoryId, now = new Date() }) { validateControlRefIntent(intent); validateControlRefApproval(approval, intent, { now }); diff --git a/scripts/lib/git-json-ledger.mjs b/scripts/lib/git-json-ledger.mjs index c429488..d114e21 100644 --- a/scripts/lib/git-json-ledger.mjs +++ b/scripts/lib/git-json-ledger.mjs @@ -30,6 +30,7 @@ export class GitJsonLedger { return result.exitCode === 0 ? result.stdout.trim() : null; } + // fallow-ignore-next-line unused-class-member async read(path) { validateLedgerPath(path); const version = await this.version(); @@ -52,6 +53,7 @@ export class GitJsonLedger { } } + // fallow-ignore-next-line unused-class-member async list(prefix = "") { if (prefix !== "") validateLedgerPath(prefix.replace(/\/$/, "")); const version = await this.version(); @@ -65,6 +67,7 @@ export class GitJsonLedger { }; } + // fallow-ignore-next-line unused-class-member async write(path, value, { expectedVersion, replacePath = null }) { validateLedgerPath(path); if (replacePath !== null) { diff --git a/scripts/lib/ledger-provider.mjs b/scripts/lib/ledger-provider.mjs index 5477f03..ca9c054 100644 --- a/scripts/lib/ledger-provider.mjs +++ b/scripts/lib/ledger-provider.mjs @@ -7,11 +7,13 @@ export class LedgerProvider { } /** @param {{repositoryId: string, baseRevision: string, headRevision: string, capturedAt?: string}} options */ + // fallow-ignore-next-line unused-class-member async snapshot(_options) { throw new Error("LedgerProvider.snapshot must be implemented."); } /** @param {unknown} snapshot */ + // fallow-ignore-next-line unused-class-member contextReferences(_snapshot) { throw new Error("LedgerProvider.contextReferences must be implemented."); } diff --git a/scripts/lib/repository-store.mjs b/scripts/lib/repository-store.mjs index 0b22b12..cec3017 100644 --- a/scripts/lib/repository-store.mjs +++ b/scripts/lib/repository-store.mjs @@ -5,11 +5,13 @@ export class RepositoryStore { } /** @param {string} revision @returns {Promise} */ + // fallow-ignore-next-line unused-class-member async listFiles(_revision) { throw new Error("RepositoryStore.listFiles must be implemented."); } /** @param {string} baseRevision @param {string} headRevision */ + // fallow-ignore-next-line unused-class-member async getDiff(_baseRevision, _headRevision) { throw new Error("RepositoryStore.getDiff must be implemented."); } @@ -20,26 +22,31 @@ export class RepositoryStore { } /** @param {string} ref */ + // fallow-ignore-next-line unused-class-member async hasRef(_ref) { throw new Error("RepositoryStore.hasRef must be implemented."); } /** @param {{path: string, branch: string, startPoint: string}} options */ + // fallow-ignore-next-line unused-class-member async createWorkspace(_options) { throw new Error("RepositoryStore.createWorkspace must be implemented."); } /** @param {{path: string, force?: boolean}} options */ + // fallow-ignore-next-line unused-class-member async removeWorkspace(_options) { throw new Error("RepositoryStore.removeWorkspace must be implemented."); } /** @param {string} revision @param {{notesRef?: string}} options */ + // fallow-ignore-next-line unused-class-member async readNote(_revision, _options = {}) { throw new Error("RepositoryStore.readNote must be implemented."); } /** @param {string} revision @param {{notesRef?: string, note: string}} options */ + // fallow-ignore-next-line unused-class-member async writeNote(_revision, _options) { throw new Error("RepositoryStore.writeNote must be implemented."); } @@ -50,6 +57,7 @@ export class RepositoryStore { } /** @param {{base: string, head: string}} options */ + // fallow-ignore-next-line unused-class-member async previewMerge(_options) { throw new Error("RepositoryStore.previewMerge must be implemented."); } @@ -60,6 +68,7 @@ export class RepositoryStore { } /** @param {{ref: string, newRevision: string, expectedOldCommit: string}} options */ + // fallow-ignore-next-line unused-class-member async fastForwardRef(_options) { throw new Error("RepositoryStore.fastForwardRef must be implemented."); } diff --git a/scripts/lib/review-cycle.mjs b/scripts/lib/review-cycle.mjs index 272cd8e..1a97d08 100644 --- a/scripts/lib/review-cycle.mjs +++ b/scripts/lib/review-cycle.mjs @@ -6,7 +6,6 @@ import { latestValidationResult } from "./validation-runner.mjs"; const REVIEW_CYCLE_SCHEMA_VERSION = "tabellio-review-cycle/v0.2"; const AGENT_REVIEW_SCHEMA_VERSION = "tabellio-agent-review/v0.1"; -const LEGACY_REVIEW_CYCLE_SCHEMA_VERSION = "tabellio-review-cycle/v0.1"; const MAX_FEEDBACK_ITEMS = 5_000; const MAX_AGENT_FINDINGS = 1_000; const MAX_TEXT_BODY = 64 * 1024; @@ -193,47 +192,6 @@ export class ReviewCycleManager { return this.#required(number); } - async migrate({ - number, - targetNumber = number, - remapCurrent = false, - apply = false, - legacyRepositoryId = null, - legacyOwner = null, - legacyRepo = null, - }) { - positiveInteger(number, "number"); - positiveInteger(targetNumber, "targetNumber"); - boolean(remapCurrent, "remapCurrent"); - boolean(apply, "apply"); - const target = { - repositoryId: this.repositoryId, - owner: this.owner, - repo: this.repo, - number: targetNumber, - }; - const source = legacyCycleIdentity({ ...target, number }, { legacyRepositoryId, legacyOwner, legacyRepo }); - const currentSource = { ...target, number }; - const paths = { - legacy: reviewCyclePaths(source).legacy, - sourceCurrent: reviewCyclePaths(currentSource).current, - current: reviewCyclePaths(target).current, - }; - const { legacy, current, migratedSource } = await readMigrationState(this.ledger, paths); - assertStableMigrationRead(legacy, current, migratedSource); - if (current.value) return currentMigrationResult({ legacy, migratedSource, current, paths, identity: target }); - const selected = selectMigrationSource({ legacy, migratedSource, paths, source, currentSource, remapCurrent }); - return migrationResult({ - ledger: this.ledger, - record: selected.record, - sourcePath: selected.path, - targetPath: paths.current, - source: selected.identity, - target, - apply, - }); - } - async #read(number) { positiveInteger(number, "number"); return this.ledger.read(cyclePath(this.repositoryId, this.owner, this.repo, number)); @@ -388,47 +346,6 @@ export function validateAgentReview(value) { return value; } -export function migrateReviewCycleV1ToV2(value, { source, target }) { - if (value?.schemaVersion === REVIEW_CYCLE_SCHEMA_VERSION) { - validateReviewCycle(value); - if (matchesCycleIdentity(value, target)) { - return { cycle: structuredClone(value), changed: false, clearedProviderUrls: 0 }; - } - assertCycleIdentity(value, source); - } else { - validateLegacyReviewCycle(value, source); - } - const cycle = structuredClone(value); - const clearedProviderUrls = cycle.checks.statuses.filter((status) => status.targetUrl !== null).length; - cycle.schemaVersion = REVIEW_CYCLE_SCHEMA_VERSION; - cycle.repository.id = target.repositoryId; - cycle.provider = { id: "github", owner: target.owner, repo: target.repo }; - cycle.changeRequest.id = pendingGitHubSyncId(value); - cycle.changeRequest.number = target.number; - cycle.changeRequest.url = githubPullRequestUrl(target.owner, target.repo, target.number); - cycle.feedback = cycle.feedback.map((item) => ({ - ...item, - source: item.source === "forgejo" ? "github" : item.source, - })); - cycle.checks.statuses = cycle.checks.statuses.map((status) => ({ ...status, targetUrl: null })); - cycle.integrity = { algorithm: "sha256", digest: cycleDigest(cycle) }; - validateReviewCycle(cycle); - assertCycleIdentity(cycle, target); - return { cycle, changed: true, clearedProviderUrls }; -} - -export function reviewCyclePaths({ repositoryId, owner, repo, number }) { - requiredString(repositoryId, "repositoryId"); - requiredString(owner, "owner"); - requiredString(repo, "repo"); - positiveInteger(number, "number"); - const suffix = `${number}-${createHash("sha256").update(`${repositoryId}\0${owner}\0${repo}`).digest("hex").slice(0, 16)}.json`; - return { - legacy: `cycles/forgejo-${suffix}`, - current: `cycles/github-${suffix}`, - }; -} - function mergeProviderFeedback(existing, incoming, timestamp) { const prior = new Map(existing.map((item) => [item.id, item])); const incomingIds = new Set(incoming.map((item) => item.id)); @@ -599,166 +516,12 @@ function cycleId(repositoryId, owner, repo, number) { } function cyclePath(repositoryId, owner, repo, number) { - return reviewCyclePaths({ repositoryId, owner, repo, number }).current; -} - -function validateLegacyReviewCycle(value, identity) { - object(value, "legacy review cycle"); - exactKeys(value, ["schemaVersion", "id", "repository", "provider", "changeRequest", "status", "round", "feedback", "fixes", "checks", "events", "createdAt", "updatedAt", "integrity"], "legacy review cycle"); - equals(value.schemaVersion, LEGACY_REVIEW_CYCLE_SCHEMA_VERSION, "legacy review cycle.schemaVersion"); - object(value.provider, "legacy review cycle.provider"); - exactKeys(value.provider, ["id", "owner", "repo"], "legacy review cycle.provider"); - equals(value.provider.id, "forgejo", "legacy review cycle.provider.id"); - if (!Array.isArray(value.feedback)) throw new Error("legacy review cycle.feedback must be an array."); - value.feedback.forEach((item, index) => member(item?.source, ["forgejo", "agent"], `legacy review cycle.feedback[${index}].source`)); - object(value.integrity, "legacy review cycle.integrity"); - exactKeys(value.integrity, ["algorithm", "digest"], "legacy review cycle.integrity"); - equals(value.integrity.algorithm, "sha256", "legacy review cycle.integrity.algorithm"); - if (!/^[0-9a-f]{64}$/.test(value.integrity.digest)) throw new Error("legacy review cycle.integrity.digest must be a SHA-256 digest."); - if (cycleDigest(value) !== value.integrity.digest) throw new Error("legacy review cycle integrity digest does not match."); - validateChangeRequest(value.changeRequest); - validateChecks(value.checks); - assertCycleIdentity(value, identity); -} - -function assertCycleIdentity(cycle, { repositoryId, owner, repo, number }) { - equals(cycle.repository.id, repositoryId, "review cycle.repository.id"); - equals(cycle.provider.owner, owner, "review cycle.provider.owner"); - equals(cycle.provider.repo, repo, "review cycle.provider.repo"); - equals(cycle.changeRequest.number, number, "review cycle.changeRequest.number"); -} - -function matchesCycleIdentity(cycle, identity) { - try { - assertCycleIdentity(cycle, identity); - return true; - } catch { - return false; - } -} - -function assertStableMigrationRead(...records) { - if (new Set(records.map((record) => record.version)).size !== 1) { - throw new Error("Review ledger changed while migration state was being read; retry."); - } -} - -async function readMigrationState(ledger, paths) { - const [legacy, current, source] = await Promise.all([ - ledger.read(paths.legacy), - ledger.read(paths.current), - readMigratedSource(ledger, paths), - ]); - return { - legacy, - current, - migratedSource: source ?? { value: null, version: current.version }, - }; -} - -function readMigratedSource(ledger, paths) { - if (paths.sourceCurrent === paths.current) return null; - return ledger.read(paths.sourceCurrent); -} - -function selectMigrationSource({ legacy, migratedSource, paths, source, currentSource, remapCurrent }) { - assertUnambiguousMigrationSources(legacy, migratedSource, paths); - if (migratedSource.value) { - requireCurrentRemap(remapCurrent, paths.sourceCurrent); - return { record: migratedSource, path: paths.sourceCurrent, identity: currentSource }; - } - if (!legacy.value) throw new Error(`Legacy review cycle ${source.number} does not exist at ${paths.legacy}.`); - return { record: legacy, path: paths.legacy, identity: source }; -} - -function requireCurrentRemap(remapCurrent, path) { - if (remapCurrent) return; - throw new Error(`Current v0.2 cycle ${path} requires --remap-current true before it can move.`); -} - -function assertUnambiguousMigrationSources(legacy, migratedSource, paths) { - if (!legacy.value || !migratedSource.value) return; - throw new Error(`Both ${paths.legacy} and ${paths.sourceCurrent} exist; refusing ambiguous migration.`); -} - -function currentMigrationResult({ legacy, migratedSource, current, paths, identity }) { - validateReviewCycle(current.value); - assertCycleIdentity(current.value, identity); - if (legacy.value || migratedSource.value) { - throw new Error(`A source cycle and ${paths.current} both exist; refusing ambiguous migration.`); - } - return { - state: "current", - changed: false, - applied: false, - sourcePath: null, - path: paths.current, - version: current.version, - requiresSync: current.value.changeRequest.id.startsWith("pending-github-sync:"), - clearedProviderUrls: 0, - cycle: current.value, - }; -} - -async function migrationResult({ ledger, record, sourcePath, targetPath, source, target, apply }) { - const migrated = migrateReviewCycleV1ToV2(record.value, { source, target }); - const paths = { legacy: sourcePath, current: targetPath }; - if (!apply) return previewMigrationResult(record, paths, migrated); - const written = await ledger.write(targetPath, migrated.cycle, { - expectedVersion: record.version, - replacePath: sourcePath, - }); - return appliedMigrationResult(written, paths, migrated); -} - -function legacyCycleIdentity(target, { legacyRepositoryId, legacyOwner, legacyRepo }) { - return { - repositoryId: legacyRepositoryId ?? target.repositoryId, - owner: legacyOwner ?? target.owner, - repo: legacyRepo ?? target.repo, - number: target.number, - }; -} - -function previewMigrationResult(legacy, paths, migrated) { - return { - state: "preview", - changed: true, - applied: false, - sourcePath: paths.legacy, - path: paths.current, - version: legacy.version, - requiresSync: true, - clearedProviderUrls: migrated.clearedProviderUrls, - cycle: migrated.cycle, - }; -} - -function appliedMigrationResult(written, paths, migrated) { - return { - state: "migrated", - changed: true, - applied: true, - sourcePath: paths.legacy, - path: paths.current, - version: written.version, - previousVersion: written.previousVersion, - requiresSync: true, - clearedProviderUrls: migrated.clearedProviderUrls, - cycle: migrated.cycle, - }; -} - -function githubPullRequestUrl(owner, repo, number) { + requiredString(repositoryId, "repositoryId"); requiredString(owner, "owner"); requiredString(repo, "repo"); positiveInteger(number, "number"); - return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pull/${number}`; -} - -function pendingGitHubSyncId(value) { - if (value.changeRequest.id.startsWith("pending-github-sync:")) return value.changeRequest.id; - return `pending-github-sync:${value.provider.id}:${value.changeRequest.id}`; + const suffix = `${number}-${createHash("sha256").update(`${repositoryId}\0${owner}\0${repo}`).digest("hex").slice(0, 16)}.json`; + return `cycles/github-${suffix}`; } function event(type, actor, at, detail) { diff --git a/scripts/lib/stack-manager.mjs b/scripts/lib/stack-manager.mjs index ba476e2..ae291ca 100644 --- a/scripts/lib/stack-manager.mjs +++ b/scripts/lib/stack-manager.mjs @@ -7,6 +7,7 @@ export class StackManager { } /** @param {{repositoryId: string, capturedAt?: string}} options */ + // fallow-ignore-next-line unused-class-member async snapshot(_options) { throw new Error("StackManager.snapshot must be implemented."); } diff --git a/scripts/providers/entire-ledger-provider.mjs b/scripts/providers/entire-ledger-provider.mjs index fcb8447..8d7f341 100644 --- a/scripts/providers/entire-ledger-provider.mjs +++ b/scripts/providers/entire-ledger-provider.mjs @@ -33,6 +33,7 @@ export class EntireLedgerProvider extends LedgerProvider { return match[1]; } + // fallow-ignore-next-line unused-class-member async snapshot({ repositoryId, baseRevision, headRevision, capturedAt = new Date().toISOString() }) { requiredString(repositoryId, "repositoryId"); const [version, baseCommit, headCommit] = await Promise.all([ @@ -87,6 +88,7 @@ export class EntireLedgerProvider extends LedgerProvider { return envelope; } + // fallow-ignore-next-line unused-class-member contextReferences(snapshot) { return checkpointReferences(snapshot); } diff --git a/scripts/providers/git-spice-operations.mjs b/scripts/providers/git-spice-operations.mjs index 8145673..7e02907 100644 --- a/scripts/providers/git-spice-operations.mjs +++ b/scripts/providers/git-spice-operations.mjs @@ -37,6 +37,7 @@ export class ApprovedGitSpiceOperations { }); } + // fallow-ignore-next-line unused-class-member async execute({ intent, approval, repositoryId = null, now = new Date() }) { validateStackOperationIntent(intent); validateStackOperationApproval(approval, intent, { now }); diff --git a/scripts/providers/git-spice-stack-manager.mjs b/scripts/providers/git-spice-stack-manager.mjs index de1d73d..dbc5696 100644 --- a/scripts/providers/git-spice-stack-manager.mjs +++ b/scripts/providers/git-spice-stack-manager.mjs @@ -34,6 +34,7 @@ export class GitSpiceStackManager extends StackManager { return normalized; } + // fallow-ignore-next-line unused-class-member async snapshot({ repositoryId, capturedAt = new Date().toISOString() }) { requiredString(repositoryId, "repositoryId"); const version = await this.toolVersion(); diff --git a/scripts/providers/native-git-store.mjs b/scripts/providers/native-git-store.mjs index ded2dbd..3375434 100644 --- a/scripts/providers/native-git-store.mjs +++ b/scripts/providers/native-git-store.mjs @@ -56,18 +56,21 @@ export class NativeGitStore extends RepositoryStore { return result.stdout.trim(); } + // fallow-ignore-next-line unused-class-member async gitConfig(key) { requiredString(key, "config key"); const result = await this.#git(["config", "--get", key], [0, 1]); return result.exitCode === 0 ? result.stdout.trim() : null; } + // fallow-ignore-next-line unused-class-member async listFiles(revision) { const commit = await this.resolveRef(revision); const result = await this.#git(["ls-tree", "-rz", "--name-only", commit, "--"]); return splitNulls(result.stdout).sort(); } + // fallow-ignore-next-line unused-class-member async getDiff(baseRevision, headRevision) { const [baseCommit, headCommit] = await Promise.all([ this.resolveRef(baseRevision), @@ -95,12 +98,14 @@ export class NativeGitStore extends RepositoryStore { return branch; } + // fallow-ignore-next-line unused-class-member async hasRef(ref) { validateFullRef(ref, "ref", "refs/"); const result = await this.#git(["show-ref", "--verify", "--quiet", ref], [0, 1]); return result.exitCode === 0; } + // fallow-ignore-next-line unused-class-member async createWorkspace({ path, branch, startPoint }) { await this.validateBranch(branch); const workspacePath = await this.#validateWorkspacePath(path); @@ -109,6 +114,7 @@ export class NativeGitStore extends RepositoryStore { return { path: workspacePath, branch, startCommit }; } + // fallow-ignore-next-line unused-class-member async removeWorkspace({ path, force = false }) { const workspacePath = await this.#validateWorkspacePath(path); const args = ["worktree", "remove"]; @@ -118,6 +124,7 @@ export class NativeGitStore extends RepositoryStore { return { path: workspacePath, removed: true }; } + // fallow-ignore-next-line unused-class-member async readNote(revision, { notesRef = "refs/notes/tabellio/context" } = {}) { validateFullRef(notesRef, "notesRef", "refs/notes/"); await this.#git(["check-ref-format", notesRef]); @@ -131,6 +138,7 @@ export class NativeGitStore extends RepositoryStore { throw new Error(`Unable to read Git note from ${notesRef}: ${result.stderr.trim()}`); } + // fallow-ignore-next-line unused-class-member async writeNote(revision, { notesRef = "refs/notes/tabellio/context", note }) { validateFullRef(notesRef, "notesRef", "refs/notes/"); requiredString(note, "note"); @@ -154,6 +162,7 @@ export class NativeGitStore extends RepositoryStore { return result.exitCode === 0; } + // fallow-ignore-next-line unused-class-member async previewMerge({ base, head }) { const [baseCommit, headCommit] = await Promise.all([this.resolveRef(base), this.resolveRef(head)]); const mergeBaseResult = await this.#git(["merge-base", baseCommit, headCommit]); @@ -201,6 +210,7 @@ export class NativeGitStore extends RepositoryStore { return { ref, oldCommit: expectedOldCommit, newCommit }; } + // fallow-ignore-next-line unused-class-member async fastForwardRef({ ref, newRevision, expectedOldCommit }) { validateFullRef(ref, "ref", "refs/heads/"); if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(expectedOldCommit ?? "")) { diff --git a/scripts/tabellio-review.mjs b/scripts/tabellio-review.mjs index f2b352d..d105982 100755 --- a/scripts/tabellio-review.mjs +++ b/scripts/tabellio-review.mjs @@ -16,7 +16,7 @@ import { ReviewCycleManager } from "./lib/review-cycle.mjs"; import { GitHubProvider } from "./providers/github-provider.mjs"; import { NativeGitStore } from "./providers/native-git-store.mjs"; -const COMMANDS = new Set(["sync", "status", "triage", "fix", "import", "migrate"]); +const COMMANDS = new Set(["sync", "status", "triage", "fix", "import"]); const COMMON_OPTIONS = ["repo", "repoId", "owner", "remoteRepo", "number", "actor", "ledgerRef", "validationLedgerRef"]; const ALLOWED_OPTIONS = { sync: [...COMMON_OPTIONS, "apiUrl", "tokenFile"], @@ -24,7 +24,6 @@ const ALLOWED_OPTIONS = { triage: [...COMMON_OPTIONS, "feedbackId", "disposition", "reason"], fix: [...COMMON_OPTIONS, "feedbackIds", "commit", "checkpoint", "summary"], import: [...COMMON_OPTIONS, "input"], - migrate: [...COMMON_OPTIONS, "apply", "targetNumber", "remapCurrent", "legacyRepoId", "legacyOwner", "legacyRemoteRepo"], }; const REQUIRED_OPTIONS = { sync: [], @@ -32,7 +31,6 @@ const REQUIRED_OPTIONS = { triage: ["feedbackId", "disposition", "reason"], fix: ["feedbackIds", "commit", "checkpoint", "summary"], import: ["input"], - migrate: [], }; const COMMAND_HANDLERS = { sync: syncReview, @@ -40,7 +38,6 @@ const COMMAND_HANDLERS = { triage: triageReview, fix: recordReviewFix, import: importAgentReview, - migrate: migrateReview, }; main().catch(reportCliError); @@ -120,18 +117,6 @@ async function importAgentReview(manager, options) { }); } -function migrateReview(manager, options) { - return manager.migrate({ - number: options.number, - targetNumber: options.targetNumber, - remapCurrent: options.remapCurrent, - apply: options.apply, - legacyRepositoryId: options.legacyRepoId, - legacyOwner: options.legacyOwner, - legacyRepo: options.legacyRemoteRepo, - }); -} - function parseArgs(args) { const command = args[0]; requireCommand(command); @@ -144,37 +129,13 @@ function parseArgs(args) { ...values, number: positiveNumberOption(values.number, "--number"), actor: defaultActor(values.actor), - ...migrationOptions(command, values), }; } function requireCommand(command) { - if (!COMMANDS.has(command)) throw new Error("Expected command: sync, status, triage, fix, import, or migrate."); + if (!COMMANDS.has(command)) throw new Error("Expected command: sync, status, triage, fix, or import."); } function defaultActor(actor) { return actor ?? process.env.USER ?? "local-agent"; } - -function migrationOptions(command, values) { - if (command !== "migrate") return {}; - return { - apply: optionalBooleanOption(values.apply, "--apply"), - remapCurrent: optionalBooleanOption(values.remapCurrent, "--remap-current"), - targetNumber: migrationTargetNumber(values), - }; -} - -function optionalBooleanOption(value, flag) { - return booleanOption(value === undefined ? "false" : value, flag); -} - -function migrationTargetNumber(values) { - if (values.targetNumber === undefined) return positiveNumberOption(values.number, "--number"); - return positiveNumberOption(values.targetNumber, "--target-number"); -} - -function booleanOption(value, flag) { - if (!["true", "false"].includes(value)) throw new Error(`${flag} must be true or false.`); - return value === "true"; -} diff --git a/tests/review-cycle.test.mjs b/tests/review-cycle.test.mjs index 06ee467..41a002b 100644 --- a/tests/review-cycle.test.mjs +++ b/tests/review-cycle.test.mjs @@ -4,9 +4,7 @@ import test from "node:test"; import { GitJsonLedger } from "../scripts/lib/git-json-ledger.mjs"; import { - migrateReviewCycleV1ToV2, ReviewCycleManager, - reviewCyclePaths, validateAgentReview, validateReviewCycle, } from "../scripts/lib/review-cycle.mjs"; @@ -192,120 +190,6 @@ test("agent review contract bounds finding count and text size", () => { assert.throws(() => validateAgentReview(input), /at most 65536/); }); -test("review-cycle v0.1 migration is deterministic and rejects tampering", async (t) => { - const fixture = await createFixture(); - t.after(() => rm(fixture.root, { recursive: true, force: true })); - const identity = { - source: legacyIdentity(), - target: { ...githubIdentity(), number: 14 }, - }; - const legacy = legacyReviewCycle(fixture); - const migrated = migrateReviewCycleV1ToV2(legacy, identity); - - assert.equal(migrated.changed, true); - assert.equal(migrated.clearedProviderUrls, 1); - assert.equal(migrated.cycle.schemaVersion, "tabellio-review-cycle/v0.2"); - assert.equal(migrated.cycle.repository.id, "IntelIP/Tabellio"); - assert.equal(migrated.cycle.provider.id, "github"); - assert.deepEqual(migrated.cycle.provider, { id: "github", owner: "IntelIP", repo: "Tabellio" }); - assert.equal(migrated.cycle.changeRequest.id, "pending-github-sync:forgejo:21"); - assert.equal(migrated.cycle.changeRequest.number, 14); - assert.equal(migrated.cycle.changeRequest.url, "https://github.com/IntelIP/Tabellio/pull/14"); - assert.equal(migrated.cycle.feedback[0].source, "github"); - assert.equal(migrated.cycle.checks.statuses[0].targetUrl, null); - assert.equal(validateReviewCycle(migrated.cycle), migrated.cycle); - - const repeated = migrateReviewCycleV1ToV2(migrated.cycle, { source: identity.target, target: identity.target }); - assert.equal(repeated.changed, false); - assert.deepEqual(repeated.cycle, migrated.cycle); - - const tampered = structuredClone(legacy); - tampered.changeRequest.title = "Tampered"; - assert.throws(() => migrateReviewCycleV1ToV2(tampered, identity), /integrity digest does not match/); -}); - -test("review-cycle migration previews, atomically moves, and becomes idempotent", async (t) => { - const { fixture, store, ledger } = await createReviewFixture(t); - const source = legacyIdentity(); - const target = { ...githubIdentity(), number: 14 }; - const paths = { - legacy: reviewCyclePaths(source).legacy, - current: reviewCyclePaths(target).current, - }; - const seeded = await ledger.write(paths.legacy, legacyReviewCycle(fixture), { expectedVersion: null }); - const manager = new ReviewCycleManager({ - store, - ledger, - provider: null, - repositoryId: target.repositoryId, - owner: target.owner, - repo: target.repo, - }); - - const migrationOptions = { - number: 7, - targetNumber: 14, - legacyRepositoryId: source.repositoryId, - legacyOwner: source.owner, - legacyRepo: source.repo, - }; - const preview = await manager.migrate(migrationOptions); - assert.equal(preview.state, "preview"); - assert.equal(preview.applied, false); - assert.equal(preview.requiresSync, true); - assert.equal(await ledger.version(), seeded.version); - - const applied = await manager.migrate({ ...migrationOptions, apply: true }); - assert.equal(applied.state, "migrated"); - assert.equal(applied.applied, true); - assert.equal((await ledger.read(paths.legacy)).value, null); - assert.equal((await ledger.read(paths.current)).value.schemaVersion, "tabellio-review-cycle/v0.2"); - assert.equal((await ledger.read(paths.current)).value.changeRequest.number, 14); - assert.deepEqual((await ledger.list("cycles")).paths, [paths.current]); - - const repeated = await manager.migrate({ ...migrationOptions, apply: true }); - assert.equal(repeated.state, "current"); - assert.equal(repeated.changed, false); - assert.equal(repeated.requiresSync, true); - assert.equal(repeated.version, applied.version); -}); - -test("review-cycle migration atomically corrects an already migrated PR number", async (t) => { - const { fixture, store, ledger } = await createReviewFixture(t); - const wrong = githubIdentity(); - const target = { ...wrong, number: 14 }; - const wrongPath = reviewCyclePaths(wrong).current; - const targetPath = reviewCyclePaths(target).current; - const migrated = migrateReviewCycleV1ToV2(legacyReviewCycle(fixture), { - source: legacyIdentity(), - target: wrong, - }).cycle; - await ledger.write(wrongPath, migrated, { expectedVersion: null }); - const manager = new ReviewCycleManager({ - store, - ledger, - provider: null, - repositoryId: target.repositoryId, - owner: target.owner, - repo: target.repo, - }); - - await assert.rejects( - manager.migrate({ number: 7, targetNumber: 14 }), - /--remap-current true/, - ); - const preview = await manager.migrate({ number: 7, targetNumber: 14, remapCurrent: true }); - assert.equal(preview.state, "preview"); - assert.equal(preview.sourcePath, wrongPath); - assert.equal(preview.path, targetPath); - assert.equal(preview.cycle.changeRequest.number, 14); - - const applied = await manager.migrate({ number: 7, targetNumber: 14, remapCurrent: true, apply: true }); - assert.equal(applied.state, "migrated"); - assert.equal((await ledger.read(wrongPath)).value, null); - assert.equal((await ledger.read(targetPath)).value.changeRequest.number, 14); -}); - async function createReviewFixture(t) { const fixture = await createFixture(); t.after(() => rm(fixture.root, { recursive: true, force: true })); @@ -469,94 +353,3 @@ function validationResult(commit, runId, status, completedAt) { value.integrity = { algorithm: "sha256", digest: digestObject(value) }; return value; } - -function legacyReviewCycle(fixture) { - const value = { - schemaVersion: "tabellio-review-cycle/v0.1", - id: "review-legacy-001", - repository: { id: "example/repository" }, - provider: { id: "forgejo", owner: "acme", repo: "project" }, - changeRequest: { - id: "21", - number: 7, - url: "https://forgejo.example.test/acme/project/pulls/7", - title: "Legacy agent change", - state: "open", - draft: false, - mergeable: true, - headBranch: "feature", - headCommit: fixture.featureCommit, - baseBranch: "main", - baseCommit: fixture.mainCommit, - updatedAt: timestamp, - }, - status: "needs_triage", - round: 2, - feedback: [{ - id: "review-comment:41", - source: "forgejo", - providerId: "41", - kind: "review-comment", - author: "reviewer", - title: "Legacy feedback", - body: "Preserve this feedback.", - path: "README.md", - line: 1, - commit: fixture.featureCommit, - severity: null, - providerState: "open", - disposition: "pending", - resolution: "open", - fixId: null, - createdAt: timestamp, - updatedAt: timestamp, - }], - fixes: [], - checks: { - commit: fixture.featureCommit, - state: "success", - total: 1, - statuses: [{ - id: "51", - context: "tests", - state: "success", - description: "Checks passed", - targetUrl: "https://forgejo.example.test/checks/51", - createdAt: timestamp, - updatedAt: timestamp, - }], - }, - events: [{ - id: "event-legacy-001", - type: "synced", - actor: "legacy-sync", - at: timestamp, - detail: "Synced legacy feedback.", - }], - createdAt: timestamp, - updatedAt: timestamp, - integrity: { algorithm: "sha256", digest: "0".repeat(64) }, - }; - value.integrity.digest = digestObject(Object.fromEntries( - Object.entries(value).filter(([key]) => key !== "integrity"), - )); - return value; -} - -function legacyIdentity() { - return { - repositoryId: "example/repository", - owner: "acme", - repo: "project", - number: 7, - }; -} - -function githubIdentity() { - return { - repositoryId: "IntelIP/Tabellio", - owner: "IntelIP", - repo: "Tabellio", - number: 7, - }; -}