feat(local): preserve terminal run history

This commit is contained in:
whyour
2026-08-23 13:30:18 +08:00
parent 5f62f2f724
commit 42262094cc
13 changed files with 1938 additions and 43 deletions
@@ -154,6 +154,13 @@ import {
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationApplyCommandFile,
} from './reconciliation/application/automation/applyCoordinator';
import {
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
readLocalReconciliationRunHistoryTerminal,
verifyLocalReconciliationRunHistory,
verifyLocalReconciliationRunHistoryCommandFile,
} from './reconciliation/application/run-history/coordinator';
import {
completeLocalReconciliation,
completeLocalReconciliationCommandFile,
@@ -199,6 +206,11 @@ export {
verifyLocalReconciliationAutomationApplyCommandFile,
rollbackLocalReconciliationAutomationApply,
rollbackLocalReconciliationAutomationApplyCommandFile,
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
readLocalReconciliationRunHistoryTerminal,
verifyLocalReconciliationRunHistory,
verifyLocalReconciliationRunHistoryCommandFile,
completeLocalReconciliation,
completeLocalReconciliationCommandFile,
verifyLocalReconciliationCompletion,
@@ -212,6 +224,8 @@ export {
type LocalReconciliationCompletionAutomationBinding,
type LocalReconciliationCompletionAutomationOptions,
type LocalReconciliationCompletionOptions,
type LocalReconciliationCompletionRunHistoryBinding,
type LocalReconciliationCompletionRunHistoryOptions,
type LocalReconciliationCompletionResult,
type LocalReconciliationCompletionVerifyCommand,
} from './reconciliation/completion/contract';
@@ -234,6 +248,23 @@ export {
} from './reconciliation/application/automation/applyContract';
export { type LocalReconciliationAutomationApplyDependencies } from './reconciliation/application/automation/applyCoordinator';
export {
normalizeLocalReconciliationRunHistoryPreserveCommand,
normalizeLocalReconciliationRunHistoryVerifyCommand,
type LocalReconciliationRunHistoryOptions,
type LocalReconciliationRunHistoryPreserveCommand,
type LocalReconciliationRunHistoryResult,
type LocalReconciliationRunHistoryVerifyCommand,
} from './reconciliation/application/run-history/contract';
export {
type LocalReconciliationRunHistoryDependencies,
type LocalReconciliationRunHistoryTerminal,
} from './reconciliation/application/run-history/coordinator';
export {
normalizeLocalReconciliationRunHistoryPreservationReceipt,
type LocalReconciliationRunHistoryPreservationReceipt,
} from './reconciliation/application/run-history/evidence';
export {
normalizeLocalReconciliationApplicationCommitCommand,
normalizeLocalReconciliationApplicationPrepareCommand,
@@ -33,6 +33,8 @@ import {
applyLocalReconciliationAutomationCommandFile,
verifyLocalReconciliationAutomationApplyCommandFile,
rollbackLocalReconciliationAutomationApplyCommandFile,
preserveLocalReconciliationRunHistoryCommandFile,
verifyLocalReconciliationRunHistoryCommandFile,
completeLocalReconciliationCommandFile,
verifyLocalReconciliationCompletionCommandFile,
writeLocalReconciliationReviewDiagnosticsCommandFile,
@@ -50,7 +52,7 @@ import {
} from './localDeployment';
const USAGE =
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|reconciliation-review-commit|reconciliation-review-verify|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-complete|reconciliation-complete-verify|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|reconciliation-review-commit|reconciliation-review-verify|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-run-history-preserve|reconciliation-run-history-verify|reconciliation-complete|reconciliation-complete-verify|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -100,6 +102,8 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-automation-apply' &&
argv[0] !== 'reconciliation-automation-apply-verify' &&
argv[0] !== 'reconciliation-automation-apply-rollback' &&
argv[0] !== 'reconciliation-run-history-preserve' &&
argv[0] !== 'reconciliation-run-history-verify' &&
argv[0] !== 'reconciliation-complete' &&
argv[0] !== 'reconciliation-complete-verify' &&
argv[0] !== 'compose-revision' &&
@@ -214,6 +218,10 @@ async function main(argv: readonly string[]): Promise<void> {
? verifyLocalReconciliationAutomationApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-automation-apply-rollback'
? rollbackLocalReconciliationAutomationApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-preserve'
? preserveLocalReconciliationRunHistoryCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-verify'
? verifyLocalReconciliationRunHistoryCommandFile(argv[2]!)
: argv[0] === 'reconciliation-complete'
? completeLocalReconciliationCommandFile(argv[2]!)
: argv[0] === 'reconciliation-complete-verify'
@@ -0,0 +1,289 @@
import path from 'node:path';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
const DIGEST = /^[0-9a-f]{64}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const SAFE_PATH = /^\/[A-Za-z0-9._/@-]+$/;
export interface LocalReconciliationRunHistoryOptions {
readonly deploymentRoot: string;
readonly applicationRoot: string;
readonly runHistoryRoot: string;
readonly allowRootService: boolean;
}
export interface LocalReconciliationRunHistoryPreserveCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.run-history.preserve';
readonly options: Readonly<LocalReconciliationRunHistoryOptions>;
readonly request: Readonly<{
preservationId: string;
applicationId: string;
expectedApplicationPlanDigest: string;
expectedHeadDigest: string;
decisionFilePath: string;
preservedAtMs: number;
}>;
}
export interface LocalReconciliationRunHistoryVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.run-history.verify';
readonly options: Readonly<LocalReconciliationRunHistoryOptions>;
readonly request: Readonly<{
preservationId: string;
applicationId: string;
expectedPreservationDigest: string;
decisionFilePath: string;
}>;
}
export interface LocalReconciliationRunHistoryResult {
readonly schemaVersion: 1;
readonly operation:
| LocalReconciliationRunHistoryPreserveCommand['operation']
| LocalReconciliationRunHistoryVerifyCommand['operation'];
readonly status: 'preserved' | 'existing' | 'verified';
readonly state: 'reconciliation_run_history_preserved';
readonly preservationId: string;
readonly applicationId: string;
readonly preservationDigest: string;
readonly legacyFactCount: number;
readonly targetFactCount: number;
}
function fail(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation run history ${message}`,
);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
fail(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
fail(`${label} shape is invalid`);
}
}
function safePath(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.parse(value).root === value ||
path.normalize(value) !== value ||
value.includes('\0') ||
value.includes('//') ||
!SAFE_PATH.test(value) ||
Buffer.byteLength(value, 'utf8') > 4_096
) {
fail(`${label} must be a safe non-root absolute path`);
}
return value;
}
function overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return (
relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))
);
}
function identifier(value: unknown, pattern: RegExp, label: string): string {
if (typeof value !== 'string' || !pattern.test(value))
fail(`${label} is invalid`);
return value;
}
function normalizeOptions(
value: unknown,
): Readonly<LocalReconciliationRunHistoryOptions> {
const selected = record(value, 'options');
exact(
selected,
['allowRootService', 'applicationRoot', 'deploymentRoot', 'runHistoryRoot'],
'options',
);
if (
typeof selected.allowRootService !== 'boolean' ||
(currentIdentity().uid === 0) !== selected.allowRootService
) {
fail('command identity is invalid');
}
const roots = [
safePath(selected.deploymentRoot, 'deploymentRoot'),
safePath(selected.applicationRoot, 'applicationRoot'),
safePath(selected.runHistoryRoot, 'runHistoryRoot'),
];
for (let left = 0; left < roots.length; left += 1) {
for (let right = left + 1; right < roots.length; right += 1) {
if (
overlaps(roots[left]!, roots[right]!) ||
overlaps(roots[right]!, roots[left]!)
) {
fail('authority roots overlap');
}
}
}
return Object.freeze({
deploymentRoot: roots[0]!,
applicationRoot: roots[1]!,
runHistoryRoot: roots[2]!,
allowRootService: selected.allowRootService,
});
}
function command(value: unknown, operation: string) {
const selected = record(value, 'command');
exact(
selected,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
fail('command version or operation is invalid');
}
return Object.freeze({
options: normalizeOptions(selected.options),
request: record(selected.request, 'request'),
});
}
function decisionFilePath(
value: unknown,
options: Readonly<LocalReconciliationRunHistoryOptions>,
): string {
const selected = safePath(value, 'decisionFilePath');
for (const root of [
options.deploymentRoot,
options.applicationRoot,
options.runHistoryRoot,
]) {
if (overlaps(root, selected) || overlaps(selected, root)) {
fail('decisionFilePath overlaps an authority root');
}
}
return selected;
}
export function normalizeLocalReconciliationRunHistoryPreserveCommand(
value: unknown,
): Readonly<LocalReconciliationRunHistoryPreserveCommand> {
const selected = command(
value,
'local.deployment.reconciliation.run-history.preserve',
);
exact(
selected.request,
[
'applicationId',
'decisionFilePath',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
'preservationId',
'preservedAtMs',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.preservedAtMs) ||
(selected.request.preservedAtMs as number) < 0
) {
fail('preservedAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.preserve',
options: selected.options,
request: Object.freeze({
preservationId: identifier(
selected.request.preservationId,
UUID_V4,
'preservationId',
),
applicationId: identifier(
selected.request.applicationId,
UUID_V4,
'applicationId',
),
expectedApplicationPlanDigest: identifier(
selected.request.expectedApplicationPlanDigest,
DIGEST,
'expectedApplicationPlanDigest',
),
expectedHeadDigest: identifier(
selected.request.expectedHeadDigest,
DIGEST,
'expectedHeadDigest',
),
decisionFilePath: decisionFilePath(
selected.request.decisionFilePath,
selected.options,
),
preservedAtMs: selected.request.preservedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationRunHistoryVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationRunHistoryVerifyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.run-history.verify',
);
exact(
selected.request,
[
'applicationId',
'decisionFilePath',
'expectedPreservationDigest',
'preservationId',
],
'request',
);
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.verify',
options: selected.options,
request: Object.freeze({
preservationId: identifier(
selected.request.preservationId,
UUID_V4,
'preservationId',
),
applicationId: identifier(
selected.request.applicationId,
UUID_V4,
'applicationId',
),
expectedPreservationDigest: identifier(
selected.request.expectedPreservationDigest,
DIGEST,
'expectedPreservationDigest',
),
decisionFilePath: decisionFilePath(
selected.request.decisionFilePath,
selected.options,
),
}),
});
}
@@ -0,0 +1,623 @@
import fs from 'node:fs';
import path from 'node:path';
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import {
preflightPublishedFile,
publishExactFile,
validatePrivateDirectory,
} from '../../../foundation/files';
import { readLocalCutoverInstanceHead } from '../../../cutover/instanceLineage';
import { readLocalReconciliationPlanTerminal } from '../../planning/preparation';
import {
assertLocalReconciliationReviewDecisionMatchesFact,
withLocalReconciliationReviewDecisionFile,
} from '../../review/decisionFile';
import { visitLocalReconciliationDiagnosticFacts } from '../../review/diagnostics';
import { readLocalReconciliationReviewTerminalJson } from '../../review/terminalEvidence';
import {
withLocalReconciliationSealedDatabase,
type LocalReconciliationSealedBundleReaderDependencies,
} from '../../sealed-bundle/reader';
import {
readLocalReconciliationApplicationTerminal,
type LocalReconciliationApplicationTerminal,
} from '../coordinator';
import {
normalizeLocalReconciliationRunHistoryPreserveCommand,
normalizeLocalReconciliationRunHistoryVerifyCommand,
type LocalReconciliationRunHistoryOptions,
type LocalReconciliationRunHistoryPreserveCommand,
type LocalReconciliationRunHistoryResult,
type LocalReconciliationRunHistoryVerifyCommand,
} from './contract';
import {
buildLocalReconciliationRunHistoryPreservationReceipt,
localReconciliationRunHistoryReceiptContents,
normalizeLocalReconciliationRunHistoryPreservationReceipt,
type LocalReconciliationRunHistoryPreservationReceipt,
} from './evidence';
const MAX_PRESERVATIONS = 64;
const MAX_RECEIPT_BYTES = 64 * 1024;
interface RunHistoryPaths {
readonly root: string;
readonly receipt: string;
}
interface RunHistoryAuthority {
readonly application: Readonly<LocalReconciliationApplicationTerminal>;
readonly runHistoryInventoryDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly decisionFileDigest: string;
readonly legacyFactCount: number;
readonly targetFactCount: number;
confirmDecisionFileIdentity(): void;
}
export interface LocalReconciliationRunHistoryDependencies
extends LocalReconciliationSealedBundleReaderDependencies {
readonly afterReceiptPublished?: () => void;
readonly afterTerminalSealed?: () => void;
}
export interface LocalReconciliationRunHistoryTerminal {
readonly receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>;
readonly application: Readonly<LocalReconciliationApplicationTerminal>;
}
function fail(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation run history ${message}`,
{ cause },
);
}
function runHistoryPaths(
runHistoryRoot: string,
preservationId: string,
): Readonly<RunHistoryPaths> {
const root = path.join(runHistoryRoot, preservationId);
return Object.freeze({ root, receipt: path.join(root, 'receipt.json') });
}
function validateDirectory(
directory: string,
uid: number,
modes: readonly number[],
label: string,
): number {
let stat: fs.Stats;
try {
stat = fs.lstatSync(directory);
} catch (error) {
return fail(`${label} is unavailable`, error);
}
const mode = stat.mode & 0o777;
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
!modes.includes(mode) ||
fs.realpathSync(directory) !== directory
) {
fail(`${label} identity is invalid`);
}
return mode;
}
function validateCatalog(selected: Readonly<RunHistoryPaths>, sealed: boolean) {
const allowed = new Set([
'receipt.json',
...(!sealed ? ['.receipt.json.ql3-deploy-stage'] : []),
]);
for (const entry of fs.readdirSync(selected.root, { withFileTypes: true })) {
if (!allowed.has(entry.name) || entry.isSymbolicLink()) {
fail('preservation catalog contains unknown material');
}
}
}
function ensurePreservationDirectory(
runHistoryRoot: string,
preservationId: string,
uid: number,
): Readonly<RunHistoryPaths> {
const selected = runHistoryPaths(runHistoryRoot, preservationId);
const entries = fs.readdirSync(runHistoryRoot, { withFileTypes: true });
if (entries.some((entry) => !entry.isDirectory() || entry.isSymbolicLink())) {
fail('preservation root catalog contains drift');
}
if (entries.length >= MAX_PRESERVATIONS && !fs.existsSync(selected.root)) {
fail('preservation retention limit is reached');
}
try {
fs.mkdirSync(selected.root, { mode: 0o700 });
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
fail('preservation directory cannot be created', error);
}
}
const mode = validateDirectory(
selected.root,
uid,
[0o700, 0o500],
'preservation directory',
);
validateCatalog(selected, mode === 0o500);
return selected;
}
function readReceipt(
selected: Readonly<RunHistoryPaths>,
uid: number,
modes: readonly number[],
): Readonly<LocalReconciliationRunHistoryPreservationReceipt> {
let stat: fs.Stats;
try {
stat = fs.lstatSync(selected.receipt);
} catch (error) {
return fail('preservation receipt is unavailable', error);
}
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
!modes.includes(stat.mode & 0o777) ||
stat.nlink !== 1 ||
stat.size < 2 ||
stat.size > MAX_RECEIPT_BYTES
) {
fail('preservation receipt identity is invalid');
}
try {
return normalizeLocalReconciliationRunHistoryPreservationReceipt(
readLocalReconciliationReviewTerminalJson(selected.receipt, uid, modes),
);
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return fail('preservation receipt cannot be read', error);
}
}
function sealPreservation(
selected: Readonly<RunHistoryPaths>,
uid: number,
): void {
validateDirectory(
selected.root,
uid,
[0o700, 0o500],
'preservation directory',
);
readReceipt(selected, uid, [0o600, 0o400]);
let descriptor = fs.openSync(
selected.receipt,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
try {
const stat = fs.fstatSync(descriptor);
if (!stat.isFile() || stat.uid !== uid || stat.nlink !== 1) {
fail('preservation receipt cannot be sealed');
}
if ((stat.mode & 0o777) !== 0o400) {
fs.fchmodSync(descriptor, 0o400);
fs.fsyncSync(descriptor);
}
} finally {
fs.closeSync(descriptor);
}
descriptor = fs.openSync(selected.root, fs.constants.O_RDONLY);
try {
const stat = fs.fstatSync(descriptor);
if ((stat.mode & 0o777) !== 0o500) fs.fchmodSync(descriptor, 0o500);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
validateCatalog(selected, true);
readReceipt(selected, uid, [0o400]);
}
function validateApplication(
application: Readonly<LocalReconciliationApplicationTerminal>,
options: Readonly<LocalReconciliationRunHistoryOptions>,
applicationId: string,
applicationPlanDigest: string,
): void {
const runHistory = application.plan.domains.find(
(domain) => domain.domain === 'run_history',
);
if (
application.intent.command.options.deploymentRoot !==
options.deploymentRoot ||
application.intent.command.options.applicationRoot !==
options.applicationRoot ||
application.plan.applicationId !== applicationId ||
application.plan.applicationPlanDigest !== applicationPlanDigest ||
!runHistory ||
runHistory.action !== 'adapter_required'
) {
fail('preservation authority is detached from the application plan');
}
}
function assertSourceHead(
application: Readonly<LocalReconciliationApplicationTerminal>,
expectedHeadDigest: string,
uid: number,
): void {
const head = readLocalCutoverInstanceHead(
application.intent.command.options.deploymentRoot,
application.intent.instanceId,
uid,
);
const automation = application.plan.domains.find(
(domain) => domain.domain === 'automation',
);
const expectedState =
automation?.action === 'no_effect'
? 'reconciliation_application_planned'
: automation?.action === 'adapter_required'
? 'reconciliation_automation_applied'
: null;
if (
expectedState === null ||
head.headDigest !== expectedHeadDigest ||
head.state !== expectedState ||
head.instanceId !== application.intent.instanceId ||
head.cutoverId !== application.intent.cutoverId ||
head.generation !== application.intent.generation ||
head.activationDigest !== application.intent.activationDigest
) {
fail('preservation lost source head compare-and-swap');
}
}
async function deriveAuthority(
options: Readonly<LocalReconciliationRunHistoryOptions>,
applicationId: string,
applicationPlanDigest: string,
decisionFilePath: string,
uid: number,
dependencies: LocalReconciliationRunHistoryDependencies,
): Promise<Readonly<RunHistoryAuthority>> {
const application = await readLocalReconciliationApplicationTerminal(
options.applicationRoot,
applicationId,
uid,
);
validateApplication(
application,
options,
applicationId,
applicationPlanDigest,
);
const planTerminal = readLocalReconciliationPlanTerminal(
application.review.intent.command.options.planRoot,
application.review.intent.command.request.planId,
uid,
);
const runHistory = planTerminal.plan.domains.find(
(domain) => domain.domain === 'run_history',
);
if (!runHistory) fail('run history inventory is absent');
let legacyFactCount = 0;
let targetFactCount = 0;
const reviewed = withLocalReconciliationReviewDecisionFile(
decisionFilePath,
{
reviewId: application.review.review.reviewId,
profile: application.plan.profile,
planDigest: planTerminal.plan.planDigest,
preparationDigest: application.review.intent.preparationDigest,
},
(cursor) => {
for (const database of ['legacy', 'target'] as const) {
const opened = withLocalReconciliationSealedDatabase(
planTerminal.bundle,
database,
uid,
dependencies,
(client) =>
visitLocalReconciliationDiagnosticFacts(
client,
database,
(fact) => {
if (fact.decisionRequirement === 'informational') return;
const decision = cursor.next();
if (decision === null) {
fail('decision file omitted a canonical fact');
}
assertLocalReconciliationReviewDecisionMatchesFact(
decision,
fact,
);
if (fact.domain !== 'run_history') return;
if (
fact.decisionRequirement !== 'required' ||
fact.reason !== 'historical_preservation_required' ||
(database === 'legacy' &&
decision.disposition !== 'retain_both') ||
(database === 'target' &&
decision.disposition !== 'retain_target')
) {
fail(
'signed review did not authorize append-only preservation',
);
}
if (database === 'legacy') legacyFactCount += 1;
else targetFactCount += 1;
},
),
);
if (opened === null) {
fail('manual-required SQLite topology cannot be preserved');
}
}
},
);
if (
reviewed.evidence.fileDigest !==
application.review.authorization.decisionFileDigest ||
reviewed.evidence.decisionCount !==
application.review.authorization.decisionCount ||
legacyFactCount < 1 ||
targetFactCount < 1
) {
fail('signed review authority drifted');
}
return Object.freeze({
application,
runHistoryInventoryDigest: runHistory.inventoryDigest,
bundleDigest: planTerminal.bundle.receipt.bundleDigest,
bundleFingerprintDigest: planTerminal.bundle.fingerprintDigest,
decisionFileDigest: reviewed.evidence.fileDigest,
legacyFactCount,
targetFactCount,
confirmDecisionFileIdentity: reviewed.confirmIdentity,
});
}
function validateReceiptBinding(
receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>,
authority: Readonly<RunHistoryAuthority>,
preservationId: string,
): void {
const application = authority.application;
if (
receipt.preservationId !== preservationId ||
receipt.applicationId !== application.plan.applicationId ||
receipt.profile !== application.plan.profile ||
receipt.instanceId !== application.intent.instanceId ||
receipt.cutoverId !== application.intent.cutoverId ||
receipt.generation !== application.intent.generation ||
receipt.activationDigest !== application.intent.activationDigest ||
receipt.applicationPlanDigest !== application.plan.applicationPlanDigest ||
receipt.reviewDigest !== application.review.review.reviewDigest ||
receipt.reviewAuthorizationDigest !==
application.review.authorization.authorizationDigest ||
receipt.reviewDecisionSetDigest !==
application.review.authorization.decisionSetDigest ||
receipt.reviewDecisionFileDigest !== authority.decisionFileDigest ||
receipt.bundleDigest !== authority.bundleDigest ||
receipt.bundleFingerprintDigest !== authority.bundleFingerprintDigest ||
receipt.runHistoryInventoryDigest !== authority.runHistoryInventoryDigest ||
receipt.legacyFactCount !== authority.legacyFactCount ||
receipt.targetFactCount !== authority.targetFactCount
) {
fail('preservation receipt is detached from terminal authority');
}
}
function result(
operation: LocalReconciliationRunHistoryResult['operation'],
status: LocalReconciliationRunHistoryResult['status'],
receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>,
): Readonly<LocalReconciliationRunHistoryResult> {
return Object.freeze({
schemaVersion: 1,
operation,
status,
state: 'reconciliation_run_history_preserved',
preservationId: receipt.preservationId,
applicationId: receipt.applicationId,
preservationDigest: receipt.preservationDigest,
legacyFactCount: receipt.legacyFactCount,
targetFactCount: receipt.targetFactCount,
});
}
export async function readLocalReconciliationRunHistoryTerminal(
options: Readonly<LocalReconciliationRunHistoryOptions>,
preservationId: string,
applicationId: string,
decisionFilePath: string,
uid: number,
dependencies: LocalReconciliationRunHistoryDependencies = {},
): Promise<Readonly<LocalReconciliationRunHistoryTerminal>> {
const selected = runHistoryPaths(options.runHistoryRoot, preservationId);
validateDirectory(selected.root, uid, [0o500], 'preservation directory');
validateCatalog(selected, true);
const receipt = readReceipt(selected, uid, [0o400]);
if (receipt.applicationId !== applicationId) {
fail('preservation path binding drifted');
}
const authority = await deriveAuthority(
options,
applicationId,
receipt.applicationPlanDigest,
decisionFilePath,
uid,
dependencies,
);
validateReceiptBinding(receipt, authority, preservationId);
authority.confirmDecisionFileIdentity();
return Object.freeze({ receipt, application: authority.application });
}
export async function preserveLocalReconciliationRunHistory(
value: unknown,
dependencies: LocalReconciliationRunHistoryDependencies = {},
): Promise<Readonly<LocalReconciliationRunHistoryResult>> {
const command = normalizeLocalReconciliationRunHistoryPreserveCommand(value);
const uid = currentIdentity().uid;
for (const [directory, label] of [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.runHistoryRoot, 'runHistoryRoot'],
] as const) {
validatePrivateDirectory(directory, uid, label);
}
const selected = ensurePreservationDirectory(
command.options.runHistoryRoot,
command.request.preservationId,
uid,
);
const authority = await deriveAuthority(
command.options,
command.request.applicationId,
command.request.expectedApplicationPlanDigest,
command.request.decisionFilePath,
uid,
dependencies,
);
let receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>;
let status: 'preserved' | 'existing' = 'preserved';
if (fs.existsSync(selected.receipt)) {
status = 'existing';
receipt = readReceipt(selected, uid, [0o600, 0o400]);
validateReceiptBinding(receipt, authority, command.request.preservationId);
if (
receipt.sourceHeadDigest !== command.request.expectedHeadDigest ||
receipt.preservedAtMs !== command.request.preservedAtMs
) {
fail('preservation command is not an exact replay');
}
} else {
assertSourceHead(
authority.application,
command.request.expectedHeadDigest,
uid,
);
if (
command.request.preservedAtMs < authority.application.plan.committedAtMs
) {
fail('preservation timestamp precedes application evidence');
}
receipt = buildLocalReconciliationRunHistoryPreservationReceipt({
preservationId: command.request.preservationId,
applicationId: command.request.applicationId,
profile: authority.application.plan.profile,
instanceId: authority.application.intent.instanceId,
cutoverId: authority.application.intent.cutoverId,
generation: authority.application.intent.generation,
activationDigest: authority.application.intent.activationDigest,
applicationPlanDigest: authority.application.plan.applicationPlanDigest,
sourceHeadDigest: command.request.expectedHeadDigest,
reviewDigest: authority.application.review.review.reviewDigest,
reviewAuthorizationDigest:
authority.application.review.authorization.authorizationDigest,
reviewDecisionSetDigest:
authority.application.review.authorization.decisionSetDigest,
reviewDecisionFileDigest: authority.decisionFileDigest,
bundleDigest: authority.bundleDigest,
bundleFingerprintDigest: authority.bundleFingerprintDigest,
runHistoryInventoryDigest: authority.runHistoryInventoryDigest,
legacyFactCount: authority.legacyFactCount,
targetFactCount: authority.targetFactCount,
preservedAtMs: command.request.preservedAtMs,
});
const contents = localReconciliationRunHistoryReceiptContents(receipt);
preflightPublishedFile(
selected.receipt,
contents,
0o600,
uid,
'run history preservation receipt',
);
authority.confirmDecisionFileIdentity();
assertSourceHead(
authority.application,
command.request.expectedHeadDigest,
uid,
);
publishExactFile(
selected.receipt,
contents,
0o600,
uid,
'run history preservation receipt',
);
dependencies.afterReceiptPublished?.();
}
sealPreservation(selected, uid);
dependencies.afterTerminalSealed?.();
authority.confirmDecisionFileIdentity();
assertSourceHead(
authority.application,
command.request.expectedHeadDigest,
uid,
);
return result(command.operation, status, receipt);
}
export async function verifyLocalReconciliationRunHistory(
value: unknown,
dependencies: LocalReconciliationRunHistoryDependencies = {},
): Promise<Readonly<LocalReconciliationRunHistoryResult>> {
const command = normalizeLocalReconciliationRunHistoryVerifyCommand(value);
const uid = currentIdentity().uid;
for (const [directory, label] of [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.runHistoryRoot, 'runHistoryRoot'],
] as const) {
validatePrivateDirectory(directory, uid, label);
}
const terminal = await readLocalReconciliationRunHistoryTerminal(
command.options,
command.request.preservationId,
command.request.applicationId,
command.request.decisionFilePath,
uid,
dependencies,
);
if (
terminal.receipt.preservationDigest !==
command.request.expectedPreservationDigest
) {
fail('verify command is detached from preservation receipt');
}
return result(command.operation, 'verified', terminal.receipt);
}
export function preserveLocalReconciliationRunHistoryCommandFile(
filePath: string,
dependencies: LocalReconciliationRunHistoryDependencies = {},
) {
return preserveLocalReconciliationRunHistory(
readPrivateLocalCommandFile(filePath),
dependencies,
);
}
export function verifyLocalReconciliationRunHistoryCommandFile(
filePath: string,
dependencies: LocalReconciliationRunHistoryDependencies = {},
) {
return verifyLocalReconciliationRunHistory(
readPrivateLocalCommandFile(filePath),
dependencies,
);
}
export type {
LocalReconciliationRunHistoryPreserveCommand,
LocalReconciliationRunHistoryVerifyCommand,
};
@@ -0,0 +1,158 @@
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import { cutoverDigest } from '../../../cutover/targetEvidence';
const RECEIPT_SCHEMA =
'qinglong3-local-reconciliation-run-history-preservation-receipt';
const DIGEST = /^[0-9a-f]{64}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
export interface LocalReconciliationRunHistoryPreservationReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_run_history_preserved';
readonly preservationId: string;
readonly applicationId: string;
readonly profile: 'edge' | 'standalone';
readonly instanceId: string;
readonly cutoverId: string;
readonly generation: number;
readonly activationDigest: string;
readonly applicationPlanDigest: string;
readonly sourceHeadDigest: string;
readonly reviewDigest: string;
readonly reviewAuthorizationDigest: string;
readonly reviewDecisionSetDigest: string;
readonly reviewDecisionFileDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly runHistoryInventoryDigest: string;
readonly legacyFactCount: number;
readonly targetFactCount: number;
readonly preservedAtMs: number;
readonly preservationDigest: string;
}
function fail(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation run history evidence ${message}`,
);
}
function exact(
value: unknown,
keys: readonly string[],
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
fail('receipt must be an object');
}
const selected = value as Record<string, unknown>;
const actual = Object.keys(selected).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
fail('receipt shape is invalid');
}
return selected;
}
export function buildLocalReconciliationRunHistoryPreservationReceipt(
input: Omit<
LocalReconciliationRunHistoryPreservationReceipt,
'schema' | 'schemaVersion' | 'state' | 'preservationDigest'
>,
): Readonly<LocalReconciliationRunHistoryPreservationReceipt> {
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_run_history_preserved' as const,
...input,
});
return Object.freeze({
...payload,
preservationDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationRunHistoryPreservationReceipt(
value: unknown,
): Readonly<LocalReconciliationRunHistoryPreservationReceipt> {
const selected = exact(value, [
'activationDigest',
'applicationId',
'applicationPlanDigest',
'bundleDigest',
'bundleFingerprintDigest',
'cutoverId',
'generation',
'instanceId',
'legacyFactCount',
'preservationDigest',
'preservationId',
'preservedAtMs',
'profile',
'reviewAuthorizationDigest',
'reviewDecisionFileDigest',
'reviewDecisionSetDigest',
'reviewDigest',
'runHistoryInventoryDigest',
'schema',
'schemaVersion',
'sourceHeadDigest',
'state',
'targetFactCount',
]);
const { preservationDigest, ...payload } = selected;
if (
selected.schema !== RECEIPT_SCHEMA ||
selected.schemaVersion !== 1 ||
selected.state !== 'reconciliation_run_history_preserved' ||
typeof selected.preservationId !== 'string' ||
!UUID_V4.test(selected.preservationId) ||
typeof selected.applicationId !== 'string' ||
!UUID_V4.test(selected.applicationId) ||
(selected.profile !== 'edge' && selected.profile !== 'standalone') ||
typeof selected.instanceId !== 'string' ||
selected.instanceId.length < 1 ||
typeof selected.cutoverId !== 'string' ||
selected.cutoverId.length < 1 ||
!Number.isSafeInteger(selected.generation) ||
(selected.generation as number) < 1 ||
![
selected.activationDigest,
selected.applicationPlanDigest,
selected.sourceHeadDigest,
selected.reviewDigest,
selected.reviewAuthorizationDigest,
selected.reviewDecisionSetDigest,
selected.reviewDecisionFileDigest,
selected.bundleDigest,
selected.bundleFingerprintDigest,
selected.runHistoryInventoryDigest,
preservationDigest,
].every(
(candidate) => typeof candidate === 'string' && DIGEST.test(candidate),
) ||
!Number.isSafeInteger(selected.legacyFactCount) ||
(selected.legacyFactCount as number) < 1 ||
!Number.isSafeInteger(selected.targetFactCount) ||
(selected.targetFactCount as number) < 1 ||
!Number.isSafeInteger(selected.preservedAtMs) ||
(selected.preservedAtMs as number) < 0 ||
cutoverDigest(payload) !== preservationDigest
) {
fail('receipt binding is invalid');
}
return Object.freeze({
...payload,
preservationDigest,
}) as unknown as Readonly<LocalReconciliationRunHistoryPreservationReceipt>;
}
export function localReconciliationRunHistoryReceiptContents(
receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>,
): string {
return `${JSON.stringify(receipt, null, 2)}\n`;
}
@@ -22,17 +22,28 @@ export interface LocalReconciliationCompletionOptions {
readonly applicationRoot: string;
readonly completionRoot: string;
readonly automation: Readonly<LocalReconciliationCompletionAutomationOptions> | null;
readonly runHistory: Readonly<LocalReconciliationCompletionRunHistoryOptions> | null;
readonly allowRootService: boolean;
}
export interface LocalReconciliationCompletionRunHistoryOptions {
readonly runHistoryRoot: string;
readonly decisionFilePath: string;
}
export interface LocalReconciliationCompletionAutomationBinding {
readonly automationId: string;
readonly decisionId: string;
readonly expectedApplyDigest: string;
}
export interface LocalReconciliationCompletionRunHistoryBinding {
readonly preservationId: string;
readonly expectedPreservationDigest: string;
}
export interface LocalReconciliationCompleteCommand {
readonly schemaVersion: 1;
readonly schemaVersion: 1 | 2;
readonly operation: 'local.deployment.reconciliation.complete';
readonly options: Readonly<LocalReconciliationCompletionOptions>;
readonly request: Readonly<{
@@ -41,12 +52,13 @@ export interface LocalReconciliationCompleteCommand {
expectedApplicationPlanDigest: string;
expectedHeadDigest: string;
automation: Readonly<LocalReconciliationCompletionAutomationBinding> | null;
runHistory: Readonly<LocalReconciliationCompletionRunHistoryBinding> | null;
completedAtMs: number;
}>;
}
export interface LocalReconciliationCompletionVerifyCommand {
readonly schemaVersion: 1;
readonly schemaVersion: 1 | 2;
readonly operation: 'local.deployment.reconciliation.complete.verify';
readonly options: Readonly<LocalReconciliationCompletionOptions>;
readonly request: Readonly<{
@@ -54,6 +66,7 @@ export interface LocalReconciliationCompletionVerifyCommand {
applicationId: string;
expectedCompletionDigest: string;
automation: Readonly<LocalReconciliationCompletionAutomationBinding> | null;
runHistory: Readonly<LocalReconciliationCompletionRunHistoryBinding> | null;
}>;
}
@@ -68,7 +81,7 @@ export interface LocalReconciliationCompletionResult {
readonly applicationId: string;
readonly completionDigest: string;
readonly domainCount: 8;
readonly adapterCount: 0 | 1;
readonly adapterCount: 0 | 1 | 2;
readonly instanceHeadDigest: string;
}
@@ -169,17 +182,27 @@ function normalizeAutomationOptions(
function normalizeOptions(
value: unknown,
schemaVersion: 1 | 2,
): Readonly<LocalReconciliationCompletionOptions> {
const selected = record(value, 'options');
exact(
selected,
[
'allowRootService',
'applicationRoot',
'automation',
'completionRoot',
'deploymentRoot',
],
schemaVersion === 1
? [
'allowRootService',
'applicationRoot',
'automation',
'completionRoot',
'deploymentRoot',
]
: [
'allowRootService',
'applicationRoot',
'automation',
'completionRoot',
'deploymentRoot',
'runHistory',
],
'options',
);
if (
@@ -189,11 +212,16 @@ function normalizeOptions(
fail('command identity is invalid');
}
const automation = normalizeAutomationOptions(selected.automation);
const runHistory =
schemaVersion === 1
? null
: normalizeRunHistoryOptions(selected.runHistory);
const normalized = Object.freeze({
deploymentRoot: safePath(selected.deploymentRoot, 'deploymentRoot'),
applicationRoot: safePath(selected.applicationRoot, 'applicationRoot'),
completionRoot: safePath(selected.completionRoot, 'completionRoot'),
automation,
runHistory,
allowRootService: selected.allowRootService,
}) as Readonly<LocalReconciliationCompletionOptions>;
const roots = [
@@ -207,6 +235,7 @@ function normalizeOptions(
automation.automationDecisionRoot,
automation.automationApplyRoot,
]),
...(runHistory === null ? [] : [runHistory.runHistoryRoot]),
];
for (let left = 0; left < roots.length; left += 1) {
for (let right = left + 1; right < roots.length; right += 1) {
@@ -228,9 +257,34 @@ function normalizeOptions(
) {
fail('targetDatabasePath overlaps an authority root');
}
if (
runHistory !== null &&
roots.some(
(root) =>
overlaps(root, runHistory.decisionFilePath) ||
overlaps(runHistory.decisionFilePath, root),
)
) {
fail('decisionFilePath overlaps an authority root');
}
return normalized;
}
function normalizeRunHistoryOptions(
value: unknown,
): Readonly<LocalReconciliationCompletionRunHistoryOptions> {
const selected = record(value, 'run history options');
exact(
selected,
['decisionFilePath', 'runHistoryRoot'],
'run history options',
);
return Object.freeze({
runHistoryRoot: safePath(selected.runHistoryRoot, 'runHistoryRoot'),
decisionFilePath: safePath(selected.decisionFilePath, 'decisionFilePath'),
});
}
function normalizeAutomationBinding(
value: unknown,
): Readonly<LocalReconciliationCompletionAutomationBinding> | null {
@@ -251,6 +305,28 @@ function normalizeAutomationBinding(
});
}
function normalizeRunHistoryBinding(
value: unknown,
): Readonly<LocalReconciliationCompletionRunHistoryBinding> {
const selected = record(value, 'run history binding');
exact(
selected,
['expectedPreservationDigest', 'preservationId'],
'run history binding',
);
return Object.freeze({
preservationId: identifier(
selected.preservationId,
UUID_V4,
'preservationId',
),
expectedPreservationDigest: digest(
selected.expectedPreservationDigest,
'expectedPreservationDigest',
),
});
}
function command(value: unknown, operation: string) {
const selected = record(value, 'command');
exact(
@@ -258,11 +334,15 @@ function command(value: unknown, operation: string) {
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
if (
(selected.schemaVersion !== 1 && selected.schemaVersion !== 2) ||
selected.operation !== operation
) {
fail('command version or operation is invalid');
}
return Object.freeze({
options: normalizeOptions(selected.options),
schemaVersion: selected.schemaVersion,
options: normalizeOptions(selected.options, selected.schemaVersion),
request: record(selected.request, 'request'),
});
}
@@ -273,14 +353,24 @@ export function normalizeLocalReconciliationCompleteCommand(
const selected = command(value, 'local.deployment.reconciliation.complete');
exact(
selected.request,
[
'applicationId',
'automation',
'completedAtMs',
'completionId',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
],
selected.schemaVersion === 1
? [
'applicationId',
'automation',
'completedAtMs',
'completionId',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
]
: [
'applicationId',
'automation',
'completedAtMs',
'completionId',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
'runHistory',
],
'request',
);
if (
@@ -290,7 +380,7 @@ export function normalizeLocalReconciliationCompleteCommand(
fail('completedAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
schemaVersion: selected.schemaVersion,
operation: 'local.deployment.reconciliation.complete',
options: selected.options,
request: Object.freeze({
@@ -313,6 +403,10 @@ export function normalizeLocalReconciliationCompleteCommand(
'expectedHeadDigest',
),
automation: normalizeAutomationBinding(selected.request.automation),
runHistory:
selected.schemaVersion === 1
? null
: normalizeRunHistoryBinding(selected.request.runHistory),
completedAtMs: selected.request.completedAtMs as number,
}),
});
@@ -327,11 +421,24 @@ export function normalizeLocalReconciliationCompletionVerifyCommand(
);
exact(
selected.request,
['applicationId', 'automation', 'completionId', 'expectedCompletionDigest'],
selected.schemaVersion === 1
? [
'applicationId',
'automation',
'completionId',
'expectedCompletionDigest',
]
: [
'applicationId',
'automation',
'completionId',
'expectedCompletionDigest',
'runHistory',
],
'request',
);
return Object.freeze({
schemaVersion: 1,
schemaVersion: selected.schemaVersion,
operation: 'local.deployment.reconciliation.complete.verify',
options: selected.options,
request: Object.freeze({
@@ -350,6 +457,10 @@ export function normalizeLocalReconciliationCompletionVerifyCommand(
'expectedCompletionDigest',
),
automation: normalizeAutomationBinding(selected.request.automation),
runHistory:
selected.schemaVersion === 1
? null
: normalizeRunHistoryBinding(selected.request.runHistory),
}),
});
}
@@ -33,6 +33,11 @@ import type {
LocalReconciliationAutomationApplyReceipt,
} from '../application/automation/applyEvidence';
import { readLocalReconciliationAutomationDecisionTerminal } from '../application/automation/decisionCoordinator';
import {
readLocalReconciliationRunHistoryTerminal,
type LocalReconciliationRunHistoryDependencies,
} from '../application/run-history/coordinator';
import type { LocalReconciliationRunHistoryPreservationReceipt } from '../application/run-history/evidence';
import type { LocalReconciliationCompletionDomainEvidence } from './evidence';
import {
buildLocalReconciliationCompletionReceipt,
@@ -63,7 +68,12 @@ interface AutomationProof {
readonly paths: ReturnType<typeof localReconciliationAutomationApplyPaths>;
}
export interface LocalReconciliationCompletionDependencies {
interface RunHistoryProof {
readonly receipt: Readonly<LocalReconciliationRunHistoryPreservationReceipt>;
}
export interface LocalReconciliationCompletionDependencies
extends LocalReconciliationRunHistoryDependencies {
readonly inspectSnapshot?: typeof inspectLocalSqliteSnapshot;
readonly afterReceiptPublished?: () => void;
readonly afterTerminalSealed?: () => void;
@@ -71,6 +81,62 @@ export interface LocalReconciliationCompletionDependencies {
readonly afterBackupCollected?: () => void;
}
async function runHistoryProof(
command: Readonly<LocalReconciliationCompleteCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
uid: number,
dependencies: LocalReconciliationCompletionDependencies,
): Promise<Readonly<RunHistoryProof> | null> {
const domain = terminal.plan.domains.find(
(selected) => selected.domain === 'run_history',
);
if (!domain) fail('run history domain is absent');
if (domain.action === 'no_effect') {
if (
command.options.runHistory !== null ||
command.request.runHistory !== null
) {
fail('no-effect completion must not carry run history authority');
}
return null;
}
if (
domain.action !== 'adapter_required' ||
command.options.runHistory === null ||
command.request.runHistory === null
) {
fail('run history domain is not terminally provable');
}
validatePrivateDirectory(
command.options.runHistory.runHistoryRoot,
uid,
'runHistoryRoot',
);
const history = await readLocalReconciliationRunHistoryTerminal(
{
deploymentRoot: command.options.deploymentRoot,
applicationRoot: command.options.applicationRoot,
runHistoryRoot: command.options.runHistory.runHistoryRoot,
allowRootService: command.options.allowRootService,
},
command.request.runHistory.preservationId,
command.request.applicationId,
command.options.runHistory.decisionFilePath,
uid,
dependencies,
);
if (
history.receipt.preservationDigest !==
command.request.runHistory.expectedPreservationDigest ||
history.receipt.applicationPlanDigest !==
terminal.plan.applicationPlanDigest ||
history.receipt.sourceHeadDigest !== command.request.expectedHeadDigest
) {
fail('run history preservation evidence is detached');
}
return Object.freeze({ receipt: history.receipt });
}
function fail(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation completion ${message}`,
@@ -393,6 +459,7 @@ async function automationProof(
function domainEvidence(
terminal: Readonly<LocalReconciliationApplicationTerminal>,
automation: Readonly<AutomationProof> | null,
runHistory: Readonly<RunHistoryProof> | null,
): readonly Readonly<LocalReconciliationCompletionDomainEvidence>[] {
return Object.freeze(
terminal.plan.domains.map((domain) => {
@@ -416,6 +483,18 @@ function domainEvidence(
evidenceDigest: automation.receipt.applyDigest,
});
}
if (
domain.domain === 'run_history' &&
domain.action === 'adapter_required' &&
runHistory !== null
) {
return Object.freeze({
domain: domain.domain,
action: 'adapter_required' as const,
evidenceKind: 'run_history_preservation' as const,
evidenceDigest: runHistory.receipt.preservationDigest,
});
}
return fail(`${domain.domain} is not terminally reconciled`);
}),
);
@@ -539,7 +618,13 @@ export async function completeLocalReconciliation(
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation);
const runHistory = await runHistoryProof(
command,
terminal,
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation, runHistory);
const selected = ensureCompletionDirectory(
command.options.completionRoot,
command.request.completionId,
@@ -572,10 +657,11 @@ export async function completeLocalReconciliation(
assertSourceHead(head, command.request.expectedHeadDigest, automation);
const adapterCount = domains.filter(
(domain) => domain.action === 'adapter_required',
).length as 0 | 1;
).length as 0 | 1 | 2;
const latestEvidenceAtMs = Math.max(
terminal.plan.committedAtMs,
automation?.receipt.appliedAtMs ?? 0,
runHistory?.receipt.preservedAtMs ?? 0,
);
if (command.request.completedAtMs < latestEvidenceAtMs) {
fail('completion timestamp precedes terminal evidence');
@@ -682,7 +768,7 @@ export async function verifyLocalReconciliationCompletion(
command.options.applicationRoot,
);
const syntheticCompleteCommand = Object.freeze({
schemaVersion: 1 as const,
schemaVersion: receipt.schemaVersion,
operation: 'local.deployment.reconciliation.complete' as const,
options: command.options,
request: Object.freeze({
@@ -691,6 +777,7 @@ export async function verifyLocalReconciliationCompletion(
expectedApplicationPlanDigest: receipt.applicationPlanDigest,
expectedHeadDigest: receipt.sourceHeadDigest,
automation: command.request.automation,
runHistory: command.request.runHistory,
completedAtMs: receipt.completedAtMs,
}),
});
@@ -700,7 +787,13 @@ export async function verifyLocalReconciliationCompletion(
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation);
const runHistory = await runHistoryProof(
syntheticCompleteCommand,
terminal,
uid,
dependencies,
);
const domains = domainEvidence(terminal, automation, runHistory);
validateReceiptBinding(
receipt,
terminal,
@@ -13,13 +13,16 @@ const UUID_V4 =
export interface LocalReconciliationCompletionDomainEvidence {
readonly domain: LocalReconciliationPlanDomain;
readonly action: 'no_effect' | 'adapter_required';
readonly evidenceKind: 'application_summary' | 'automation_apply';
readonly evidenceKind:
| 'application_summary'
| 'automation_apply'
| 'run_history_preservation';
readonly evidenceDigest: string;
}
export interface LocalReconciliationCompletionReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly schemaVersion: 1 | 2;
readonly state: 'reconciliation_completed';
readonly completionId: string;
readonly applicationId: string;
@@ -31,7 +34,7 @@ export interface LocalReconciliationCompletionReceipt {
readonly applicationPlanDigest: string;
readonly sourceHeadDigest: string;
readonly domains: readonly Readonly<LocalReconciliationCompletionDomainEvidence>[];
readonly adapterCount: 0 | 1;
readonly adapterCount: 0 | 1 | 2;
readonly completedAtMs: number;
readonly completionDigest: string;
}
@@ -65,6 +68,7 @@ function exact(
function domainEvidence(
value: unknown,
expectedDomain: LocalReconciliationPlanDomain,
schemaVersion: 1 | 2,
): Readonly<LocalReconciliationCompletionDomainEvidence> {
const selected = exact(
value,
@@ -78,9 +82,14 @@ function domainEvidence(
expectedDomain === 'automation' &&
selected.action === 'adapter_required' &&
selected.evidenceKind === 'automation_apply';
const runHistory =
schemaVersion === 2 &&
expectedDomain === 'run_history' &&
selected.action === 'adapter_required' &&
selected.evidenceKind === 'run_history_preservation';
if (
selected.domain !== expectedDomain ||
(!noEffect && !automation) ||
(!noEffect && !automation && !runHistory) ||
typeof selected.evidenceDigest !== 'string' ||
!DIGEST.test(selected.evidenceDigest)
) {
@@ -100,9 +109,14 @@ export function buildLocalReconciliationCompletionReceipt(
'schema' | 'schemaVersion' | 'state' | 'completionDigest'
>,
): Readonly<LocalReconciliationCompletionReceipt> {
const schemaVersion = input.domains.some(
(domain) => domain.evidenceKind === 'run_history_preservation',
)
? (2 as const)
: (1 as const);
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
schemaVersion,
state: 'reconciliation_completed' as const,
...input,
});
@@ -140,10 +154,14 @@ export function normalizeLocalReconciliationCompletionReceipt(
if (!Array.isArray(selected.domains) || selected.domains.length !== 8) {
fail('receipt domain catalog is invalid');
}
if (selected.schemaVersion !== 1 && selected.schemaVersion !== 2) {
fail('receipt schema version is invalid');
}
const schemaVersion = selected.schemaVersion;
const rawDomains = selected.domains as unknown[];
const domains = Object.freeze(
LOCAL_RECONCILIATION_PLAN_DOMAINS.map((domain, index) =>
domainEvidence(rawDomains[index], domain),
domainEvidence(rawDomains[index], domain, schemaVersion),
),
);
const adapterCount = domains.filter(
@@ -153,7 +171,10 @@ export function normalizeLocalReconciliationCompletionReceipt(
const normalized = Object.freeze({ ...raw, domains });
if (
selected.schema !== RECEIPT_SCHEMA ||
selected.schemaVersion !== 1 ||
(schemaVersion === 2) !==
domains.some(
(domain) => domain.evidenceKind === 'run_history_preservation',
) ||
selected.state !== 'reconciliation_completed' ||
typeof selected.completionId !== 'string' ||
!UUID_V4.test(selected.completionId) ||
@@ -174,7 +195,7 @@ export function normalizeLocalReconciliationCompletionReceipt(
].every(
(candidate) => typeof candidate === 'string' && DIGEST.test(candidate),
) ||
(selected.adapterCount !== 0 && selected.adapterCount !== 1) ||
![0, 1, 2].includes(selected.adapterCount as number) ||
selected.adapterCount !== adapterCount ||
!Number.isSafeInteger(selected.completedAtMs) ||
(selected.completedAtMs as number) < 0 ||
@@ -31,6 +31,7 @@ export type LocalReconciliationDiagnosticReason =
| 'reviewable_fact'
| 'unknown_schema'
| 'secret_custody_required'
| 'historical_preservation_required'
| 'historical_integrity_required'
| 'identity_custody_required';
@@ -89,7 +90,83 @@ function quotedIdentifier(name: string): string {
return `"${name.replaceAll('"', '""')}"`;
}
function requirement(domain: LocalReconciliationPlanDomain): Readonly<{
function targetRunHistoryIsTerminal(client: DatabaseSync): boolean {
const tables = new Map<string, Set<string>>();
for (const tableName of ['Runs', 'RunAttempts', 'StepRuns'] as const) {
const table = client
.prepare(
`SELECT 1 AS present
FROM sqlite_schema
WHERE type = 'table' AND name = ?
LIMIT 1`,
)
.get(tableName) as { readonly present?: unknown } | undefined;
if (table?.present !== 1) continue;
const columns = new Set<string>();
const rows = client
.prepare(`SELECT name FROM pragma_table_info(?) ORDER BY cid LIMIT 257`)
.iterate(tableName) as IterableIterator<{ readonly name?: unknown }>;
let count = 0;
for (const row of rows) {
count += 1;
if (
count > 256 ||
typeof row.name !== 'string' ||
Buffer.byteLength(row.name, 'utf8') > MAX_NAME_BYTES
) {
return false;
}
columns.add(row.name);
}
tables.set(tableName, columns);
}
const runs = tables.get('Runs');
if (!runs?.has('status') || !runs.has('finished_at_ms')) return false;
const activeRun = client
.prepare(
`SELECT 1 AS present
FROM "Runs"
WHERE status NOT IN ('succeeded','failed','cancelled','timed_out')
OR finished_at_ms IS NULL
LIMIT 1`,
)
.get() as { readonly present?: unknown } | undefined;
if (activeRun?.present === 1) return false;
const attempts = tables.get('RunAttempts');
if (attempts !== undefined) {
if (!attempts.has('status')) return false;
const activeAttempt = client
.prepare(
`SELECT 1 AS present
FROM "RunAttempts"
WHERE status IN ('claimed','starting','running')
LIMIT 1`,
)
.get() as { readonly present?: unknown } | undefined;
if (activeAttempt?.present === 1) return false;
}
const steps = tables.get('StepRuns');
if (steps !== undefined) {
if (!steps.has('status') || !steps.has('finished_at_ms')) return false;
const activeStep = client
.prepare(
`SELECT 1 AS present
FROM "StepRuns"
WHERE status NOT IN ('succeeded','failed','skipped','cancelled','timed_out')
OR finished_at_ms IS NULL
LIMIT 1`,
)
.get() as { readonly present?: unknown } | undefined;
if (activeStep?.present === 1) return false;
}
return true;
}
function requirement(
client: DatabaseSync,
database: LocalReconciliationSealedDatabaseKind,
domain: LocalReconciliationPlanDomain,
): Readonly<{
decisionRequirement: LocalReconciliationDiagnosticDecisionRequirement;
reason: LocalReconciliationDiagnosticReason;
}> {
@@ -106,6 +183,12 @@ function requirement(domain: LocalReconciliationPlanDomain): Readonly<{
});
}
if (domain === 'run_history') {
if (database === 'legacy' || targetRunHistoryIsTerminal(client)) {
return Object.freeze({
decisionRequirement: 'required' as const,
reason: 'historical_preservation_required' as const,
});
}
return Object.freeze({
decisionRequirement: 'blocked' as const,
reason: 'historical_integrity_required' as const,
@@ -161,8 +244,11 @@ function fact(
name: string,
tableName: string,
count: string | null,
review: Readonly<{
decisionRequirement: LocalReconciliationDiagnosticDecisionRequirement;
reason: LocalReconciliationDiagnosticReason;
}>,
): Readonly<LocalReconciliationDiagnosticFact> {
const review = requirement(domain);
const payload = Object.freeze({
schema: FACT_SCHEMA,
schemaVersion: 1 as const,
@@ -190,6 +276,7 @@ function schemaFacts(
records: readonly Readonly<LocalReconciliationDiagnosticFact>[];
complete: boolean;
}> {
const review = requirement(client, database, domain);
const rows = client
.prepare(
`SELECT type, name, tbl_name AS tableName
@@ -240,6 +327,7 @@ function schemaFacts(
row.name,
row.tableName,
null,
review,
),
);
}
@@ -256,6 +344,7 @@ function tableFacts(
records: readonly Readonly<LocalReconciliationDiagnosticFact>[];
complete: boolean;
}> {
const review = requirement(client, database, domain);
const rows = client
.prepare(
`SELECT name, type
@@ -302,6 +391,7 @@ function tableFacts(
row.name,
row.name,
rowCount(client, domain, row.name, row.type),
review,
),
);
}
@@ -18,6 +18,7 @@ const {
prepareLocalReconciliationCapture,
prepareLocalReconciliationApplication,
prepareLocalReconciliationAutomationDecision,
preserveLocalReconciliationRunHistory,
readLocalReconciliationAutomationDecisionTerminal,
rollbackLocalReconciliationAutomationApply,
planLocalReconciliationAutomation,
@@ -31,6 +32,7 @@ const {
verifyLocalReconciliationCompletion,
verifyLocalReconciliationPlan,
verifyLocalReconciliationReview,
verifyLocalReconciliationRunHistory,
writeLocalReconciliationReviewDiagnostics,
} = require('../dist/deployment/localDeployment.js');
const {
@@ -710,6 +712,50 @@ function automationDatabaseInitializer() {
};
}
function runHistoryDatabaseInitializer() {
return ({ legacySourcePath, recoveryPath, targetDatabasePath }) => {
const legacy = new DatabaseSync(legacySourcePath);
legacy.exec(`
CREATE TABLE "CrontabStats" (
id INTEGER PRIMARY KEY,
timestamp INTEGER NOT NULL,
status INTEGER NOT NULL
);
INSERT INTO "CrontabStats" (id, timestamp, status)
VALUES (1, 1000, 0);
`);
legacy.close();
fs.chmodSync(legacySourcePath, 0o600);
fs.copyFileSync(legacySourcePath, recoveryPath);
fs.chmodSync(recoveryPath, 0o600);
const target = new DatabaseSync(targetDatabasePath);
target.exec(`
CREATE TABLE "Runs" (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
finished_at_ms INTEGER
);
INSERT INTO "Runs" (id, status, finished_at_ms)
VALUES ('baseline', 'succeeded', 1000);
`);
target.close();
fs.chmodSync(targetDatabasePath, 0o600);
};
}
function mutateRunHistoryTarget({ targetDatabasePath }, status = 'failed') {
const target = new DatabaseSync(targetDatabasePath);
target
.prepare(
`INSERT INTO "Runs" (id, status, finished_at_ms)
VALUES (?, ?, ?)`,
)
.run('captured', status, status === 'running' ? null : 2000);
target.close();
return Object.freeze({});
}
function automationReadyDatabaseInitializer() {
const initializeLegacy = automationDatabaseInitializer();
return (paths) => {
@@ -3244,6 +3290,303 @@ test('completion fence retains automation rollback backup while other domains re
);
});
test('run history preservation seals terminal histories and completes through v2 evidence', async (t) => {
const state = await reviewedApplicationFixture(t, {
planId: '00000000-0000-4000-8000-000000000501',
reviewId: '00000000-0000-4000-8000-000000000502',
applicationId: '00000000-0000-4000-8000-000000000503',
reviewSuffix: 'run-history-preservation',
createDefaultSidecars: false,
initializeDatabases: runHistoryDatabaseInitializer(),
mutateTarget(paths) {
return mutateRunHistoryTarget(paths);
},
mutateDecisions(records) {
const selected = records.filter(
(record) =>
record.kind === 'qinglong3-local-reconciliation-review-decision' &&
record.database === 'legacy' &&
record.domain === 'run_history',
);
assert.ok(selected.length > 0);
for (const record of selected) {
record.disposition = 'retain_both';
record.reason = 'preserve_both';
}
},
});
const prepared = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
const application = await commitLocalReconciliationApplication(
applicationCommitCommand(state, prepared),
);
assert.equal(application.outcome, 'adapter_required');
const applicationPlan = JSON.parse(
fs.readFileSync(
path.join(state.applicationRoot, application.applicationId, 'plan.json'),
'utf8',
),
);
assert.equal(
applicationPlan.domains.find((domain) => domain.domain === 'run_history')
.action,
'adapter_required',
);
const runHistoryRoot = path.join(
path.dirname(state.captureRoot),
'run-history-preservation-root',
);
fs.mkdirSync(runHistoryRoot, { mode: 0o700 });
const preservationCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.preserve',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
runHistoryRoot,
allowRootService: rootAcknowledgement(),
},
request: {
preservationId: '00000000-0000-4000-8000-000000000504',
applicationId: application.applicationId,
expectedApplicationPlanDigest: application.applicationPlanDigest,
expectedHeadDigest: application.instanceHeadDigest,
decisionFilePath: state.reviewFile.filePath,
preservedAtMs: state.prepareApplicationCommand.request.preparedAtMs + 3,
},
};
for (const boundary of ['afterReceiptPublished', 'afterTerminalSealed']) {
await assert.rejects(
preserveLocalReconciliationRunHistory(preservationCommand, {
[boundary]() {
throw new Error(`run history ${boundary} response loss`);
},
}),
new RegExp(`run history ${boundary} response loss`),
);
}
const preserved = await preserveLocalReconciliationRunHistory(
preservationCommand,
);
assert.equal(preserved.status, 'existing');
assert.ok(preserved.legacyFactCount > 0);
assert.ok(preserved.targetFactCount > 0);
const preservationDirectory = path.join(
runHistoryRoot,
preservationCommand.request.preservationId,
);
assert.deepEqual(fs.readdirSync(preservationDirectory), ['receipt.json']);
assert.equal(fs.statSync(preservationDirectory).mode & 0o777, 0o500);
assert.equal(
fs.statSync(path.join(preservationDirectory, 'receipt.json')).mode & 0o777,
0o400,
);
const preservationReceiptText = fs.readFileSync(
path.join(preservationDirectory, 'receipt.json'),
'utf8',
);
assert.equal(preservationReceiptText.includes('CrontabStats'), false);
assert.equal(preservationReceiptText.includes('captured'), false);
assert.equal(
preservationReceiptText.includes(state.reviewFile.filePath),
false,
);
const preservationVerifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.run-history.verify',
options: preservationCommand.options,
request: {
preservationId: preservationCommand.request.preservationId,
applicationId: preservationCommand.request.applicationId,
expectedPreservationDigest: preserved.preservationDigest,
decisionFilePath: state.reviewFile.filePath,
},
};
const preservationVerified = await verifyLocalReconciliationRunHistory(
preservationVerifyCommand,
);
assert.equal(preservationVerified.status, 'verified');
const completionRoot = path.join(
path.dirname(state.captureRoot),
'completion-run-history',
);
fs.mkdirSync(completionRoot, { mode: 0o700 });
const runHistory = {
preservationId: preservationCommand.request.preservationId,
expectedPreservationDigest: preserved.preservationDigest,
};
const completionCommand = {
schemaVersion: 2,
operation: 'local.deployment.reconciliation.complete',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
completionRoot,
automation: null,
runHistory: {
runHistoryRoot,
decisionFilePath: state.reviewFile.filePath,
},
allowRootService: rootAcknowledgement(),
},
request: {
completionId: '00000000-0000-4000-8000-000000000505',
applicationId: application.applicationId,
expectedApplicationPlanDigest: application.applicationPlanDigest,
expectedHeadDigest: application.instanceHeadDigest,
automation: null,
runHistory,
completedAtMs: preservationCommand.request.preservedAtMs + 1,
},
};
const completed = await completeLocalReconciliation(completionCommand);
assert.equal(completed.adapterCount, 1);
const completionReceipt = JSON.parse(
fs.readFileSync(
path.join(
completionRoot,
completionCommand.request.completionId,
'receipt.json',
),
'utf8',
),
);
assert.equal(completionReceipt.schemaVersion, 2);
assert.deepEqual(
completionReceipt.domains.find((domain) => domain.domain === 'run_history'),
{
domain: 'run_history',
action: 'adapter_required',
evidenceKind: 'run_history_preservation',
evidenceDigest: preserved.preservationDigest,
},
);
const completionVerified = await verifyLocalReconciliationCompletion({
schemaVersion: 2,
operation: 'local.deployment.reconciliation.complete.verify',
options: completionCommand.options,
request: {
completionId: completionCommand.request.completionId,
applicationId: completionCommand.request.applicationId,
expectedCompletionDigest: completed.completionDigest,
automation: null,
runHistory,
},
});
assert.equal(completionVerified.status, 'verified');
const commandPath = path.join(
state.deploymentRoot,
'run-history-verify.json',
);
fs.writeFileSync(
commandPath,
`${JSON.stringify(preservationVerifyCommand)}\n`,
{ mode: 0o600 },
);
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-run-history-verify',
'--command-file',
commandPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
assert.equal(JSON.parse(cli.stdout).status, 'verified');
assert.equal(cli.stdout.includes(runHistoryRoot), false);
assert.equal(cli.stdout.includes('CrontabStats'), false);
const preservationReceiptPath = path.join(
preservationDirectory,
'receipt.json',
);
const tamperedReceipt = JSON.parse(
fs.readFileSync(preservationReceiptPath, 'utf8'),
);
tamperedReceipt.legacyFactCount += 1;
fs.chmodSync(preservationDirectory, 0o700);
fs.chmodSync(preservationReceiptPath, 0o600);
fs.writeFileSync(
preservationReceiptPath,
`${JSON.stringify(tamperedReceipt, null, 2)}\n`,
);
fs.chmodSync(preservationReceiptPath, 0o400);
fs.chmodSync(preservationDirectory, 0o500);
await assert.rejects(
verifyLocalReconciliationRunHistory(preservationVerifyCommand),
/receipt binding is invalid/,
);
});
test('run history preservation rejects active target runs and review escalation', async (t) => {
const state = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000511',
reviewId: '00000000-0000-4000-8000-000000000512',
reviewSuffix: 'run-history-active',
createDefaultSidecars: false,
initializeDatabases: runHistoryDatabaseInitializer(),
mutateTarget(paths) {
return mutateRunHistoryTarget(paths, 'running');
},
});
const pageCommand = diagnosticCommand(state, state.prepared, {
database: 'target',
domain: 'run_history',
factKind: 'table',
outputName: 'active-run-history.json',
});
writeLocalReconciliationReviewDiagnostics(pageCommand);
const page = JSON.parse(
fs.readFileSync(pageCommand.request.outputPath, 'utf8'),
);
assert.ok(page.records.length > 0);
assert.equal(
page.records.every(
(fact) =>
fact.decisionRequirement === 'blocked' &&
fact.reason === 'historical_integrity_required',
),
true,
);
const selected = state.reviewFile.records.find(
(record) =>
record.kind === 'qinglong3-local-reconciliation-review-decision' &&
record.database === 'target' &&
record.domain === 'run_history',
);
assert.ok(selected);
selected.disposition = 'retain_target';
selected.reason = 'preserve_target';
fs.writeFileSync(
state.reviewFile.filePath,
`${state.reviewFile.records
.map((record) => JSON.stringify(record))
.join('\n')}\n`,
{ mode: 0o600 },
);
await assert.rejects(
commitLocalReconciliationReview(state.command, state.dependencies),
/decision disposition is not allowed for canonical fact/,
);
assert.equal(
fs.existsSync(
path.join(
state.reviewRoot,
state.reviewCommand.request.reviewId,
'review.json',
),
),
false,
);
});
test('automation adapter builds a sealed row plan with bounded conflict evidence', async (t) => {
const state = await reviewedApplicationFixture(t, {
planId: '00000000-0000-4000-8000-000000000431',