feat(ql3): plan reconciliation application

This commit is contained in:
whyour
2026-08-21 20:24:41 +08:00
parent 22f58b1eff
commit c218f71250
14 changed files with 2634 additions and 5 deletions
@@ -29,6 +29,8 @@ export type LocalCutoverInstanceHeadState =
| 'reconciliation_planned'
| 'reconciliation_review_prepared'
| 'reconciliation_reviewed'
| 'reconciliation_application_prepared'
| 'reconciliation_application_planned'
| 'rollback_prepared'
| 'legacy_restart_requested'
| 'legacy_running'
@@ -166,6 +168,8 @@ function parseHead(value: unknown): Readonly<LocalCutoverInstanceHead> {
head.state !== 'reconciliation_planned' &&
head.state !== 'reconciliation_review_prepared' &&
head.state !== 'reconciliation_reviewed' &&
head.state !== 'reconciliation_application_prepared' &&
head.state !== 'reconciliation_application_planned' &&
head.state !== 'rollback_prepared' &&
head.state !== 'legacy_restart_requested' &&
head.state !== 'legacy_running' &&
@@ -338,6 +342,8 @@ export function advanceLocalCutoverInstanceHead(
| 'reconciliation_planned'
| 'reconciliation_review_prepared'
| 'reconciliation_reviewed'
| 'reconciliation_application_prepared'
| 'reconciliation_application_planned'
| 'rollback_prepared'
| 'legacy_restart_requested'
| 'legacy_running'
@@ -382,6 +388,8 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_planned' ||
current.state === 'reconciliation_review_prepared' ||
current.state === 'reconciliation_reviewed' ||
current.state === 'reconciliation_application_prepared' ||
current.state === 'reconciliation_application_planned' ||
current.state === 'legacy_restart_requested' ||
current.state === 'legacy_running' ||
current.state === 'legacy_ready')
@@ -409,6 +417,10 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_planned') ||
(state === 'reconciliation_reviewed' &&
current.state === 'reconciliation_review_prepared') ||
(state === 'reconciliation_application_prepared' &&
current.state === 'reconciliation_reviewed') ||
(state === 'reconciliation_application_planned' &&
current.state === 'reconciliation_application_prepared') ||
(state === 'rollback_prepared' && current.state === 'target_stopped') ||
(state === 'legacy_restart_requested' &&
current.state === 'rollback_prepared') ||
@@ -123,6 +123,14 @@ import {
verifyLocalReconciliationReview,
verifyLocalReconciliationReviewCommandFile,
} from './reconciliation/review/completion';
import {
commitLocalReconciliationApplication,
commitLocalReconciliationApplicationCommandFile,
prepareLocalReconciliationApplication,
prepareLocalReconciliationApplicationCommandFile,
verifyLocalReconciliationApplication,
verifyLocalReconciliationApplicationCommandFile,
} from './reconciliation/application/coordinator';
export {
commitLocalReconciliationPlan,
@@ -139,8 +147,42 @@ export {
commitLocalReconciliationReviewCommandFile,
verifyLocalReconciliationReview,
verifyLocalReconciliationReviewCommandFile,
prepareLocalReconciliationApplication,
prepareLocalReconciliationApplicationCommandFile,
commitLocalReconciliationApplication,
commitLocalReconciliationApplicationCommandFile,
verifyLocalReconciliationApplication,
verifyLocalReconciliationApplicationCommandFile,
};
export {
normalizeLocalReconciliationApplicationCommitCommand,
normalizeLocalReconciliationApplicationPrepareCommand,
normalizeLocalReconciliationApplicationVerifyCommand,
type LocalReconciliationApplicationCommitCommand,
type LocalReconciliationApplicationOptions,
type LocalReconciliationApplicationPrepareCommand,
type LocalReconciliationApplicationPrepareResult,
type LocalReconciliationApplicationTerminalResult,
type LocalReconciliationApplicationVerifyCommand,
} from './reconciliation/application/contract';
export {
localReconciliationApplicationDirectory,
normalizeLocalReconciliationApplicationIntent,
readLocalReconciliationApplicationIntent,
type LocalReconciliationApplicationDependencies,
type LocalReconciliationApplicationIntent,
} from './reconciliation/application/coordinator';
export {
normalizeLocalReconciliationApplicationPlan,
normalizeLocalReconciliationApplicationPlanReceipt,
type LocalReconciliationApplicationDatabaseDecisionSummary,
type LocalReconciliationApplicationDomainAction,
type LocalReconciliationApplicationDomainSummary,
type LocalReconciliationApplicationPlan,
type LocalReconciliationApplicationPlanReceipt,
} from './reconciliation/application/plan';
export {
LocalDeploymentConfigurationError,
normalizeLocalDeploymentComposeApplyCommand,
@@ -22,6 +22,9 @@ import {
verifyLocalReconciliationCaptureCommandFile,
verifyLocalReconciliationPlanCommandFile,
verifyLocalReconciliationReviewCommandFile,
prepareLocalReconciliationApplicationCommandFile,
commitLocalReconciliationApplicationCommandFile,
verifyLocalReconciliationApplicationCommandFile,
writeLocalReconciliationReviewDiagnosticsCommandFile,
prepareLocalDeploymentCommandFile,
proveLocalDeploymentLegacyReadinessCommandFile,
@@ -37,7 +40,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|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|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')) {
@@ -76,6 +79,9 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-review-diagnostics' &&
argv[0] !== 'reconciliation-review-commit' &&
argv[0] !== 'reconciliation-review-verify' &&
argv[0] !== 'reconciliation-application-prepare' &&
argv[0] !== 'reconciliation-application-commit' &&
argv[0] !== 'reconciliation-application-verify' &&
argv[0] !== 'compose-revision' &&
argv[0] !== 'compose-preflight' &&
argv[0] !== 'compose-apply' &&
@@ -166,6 +172,12 @@ async function main(argv: readonly string[]): Promise<void> {
? commitLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-verify'
? verifyLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'reconciliation-application-prepare'
? prepareLocalReconciliationApplicationCommandFile(argv[2]!)
: argv[0] === 'reconciliation-application-commit'
? commitLocalReconciliationApplicationCommandFile(argv[2]!)
: argv[0] === 'reconciliation-application-verify'
? verifyLocalReconciliationApplicationCommandFile(argv[2]!)
: argv[0] === 'compose-revision'
? switchLocalDeploymentComposeRevisionCommandFile(argv[2]!)
: argv[0] === 'compose-preflight'
@@ -0,0 +1,372 @@
import path from 'node:path';
import { currentIdentity } from '../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const SAFE_PATH_PATTERN = /^\/[A-Za-z0-9._/@-]+$/;
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const MAX_PATH_BYTES = 4_096;
export interface LocalReconciliationApplicationOptions {
readonly deploymentRoot: string;
readonly captureRoot: string;
readonly planRoot: string;
readonly reviewRoot: string;
readonly applicationRoot: string;
readonly issuerKeyringPath: string;
readonly allowRootService: boolean;
}
export interface LocalReconciliationApplicationPrepareCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.application.prepare';
readonly options: Readonly<LocalReconciliationApplicationOptions>;
readonly request: Readonly<{
applicationId: string;
reviewId: string;
expectedReviewDigest: string;
expectedHeadDigest: string;
preparedAtMs: number;
}>;
}
export interface LocalReconciliationApplicationCommitCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.application.commit';
readonly options: Readonly<LocalReconciliationApplicationOptions>;
readonly request: Readonly<{
applicationId: string;
expectedPreparationDigest: string;
expectedHeadDigest: string;
committedAtMs: number;
}>;
}
export interface LocalReconciliationApplicationVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.application.verify';
readonly options: Readonly<LocalReconciliationApplicationOptions>;
readonly request: Readonly<{
applicationId: string;
expectedApplicationPlanDigest: string;
}>;
}
export interface LocalReconciliationApplicationPrepareResult {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.application.prepare';
readonly status: 'prepared' | 'existing';
readonly state: 'reconciliation_application_prepared';
readonly applicationId: string;
readonly preparationDigest: string;
readonly instanceHeadDigest: string;
}
export interface LocalReconciliationApplicationTerminalResult {
readonly schemaVersion: 1;
readonly operation:
| 'local.deployment.reconciliation.application.commit'
| 'local.deployment.reconciliation.application.verify';
readonly status: 'prepared' | 'existing' | 'verified';
readonly state: 'reconciliation_application_planned';
readonly applicationId: string;
readonly applicationPlanDigest: string;
readonly outcome:
| 'no_effect_ready'
| 'adapter_required'
| 'manual_required'
| 'adapter_and_manual_required';
readonly domainCount: 8;
readonly instanceHeadDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(message);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
configurationError(`${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])
) {
configurationError(`${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_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
) {
configurationError(`${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 descendant(root: string, candidate: string, label: string): void {
const relative = path.relative(root, candidate);
if (
relative.length === 0 ||
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
configurationError(`${label} must be a descendant of deploymentRoot`);
}
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
configurationError(`${label} must be a SHA-256 digest`);
}
return value;
}
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) {
configurationError(`${label} must be a UUID v4`);
}
return value;
}
function normalizeOptions(
value: unknown,
): Readonly<LocalReconciliationApplicationOptions> {
const options = object(value, 'options');
exact(
options,
[
'allowRootService',
'applicationRoot',
'captureRoot',
'deploymentRoot',
'issuerKeyringPath',
'planRoot',
'reviewRoot',
],
'options',
);
const identity = currentIdentity();
if (
typeof options.allowRootService !== 'boolean' ||
(identity.uid === 0) !== options.allowRootService
) {
configurationError(
'reconciliation application command identity is invalid',
);
}
const roots = [
safePath(options.deploymentRoot, 'deploymentRoot'),
safePath(options.captureRoot, 'captureRoot'),
safePath(options.planRoot, 'planRoot'),
safePath(options.reviewRoot, 'reviewRoot'),
safePath(options.applicationRoot, 'applicationRoot'),
];
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]!)
) {
configurationError(
'deployment, capture, plan, review and application roots must not overlap',
);
}
}
}
const issuerKeyringPath = safePath(
options.issuerKeyringPath,
'issuerKeyringPath',
);
descendant(roots[0]!, issuerKeyringPath, 'issuerKeyringPath');
return Object.freeze({
deploymentRoot: roots[0]!,
captureRoot: roots[1]!,
planRoot: roots[2]!,
reviewRoot: roots[3]!,
applicationRoot: roots[4]!,
issuerKeyringPath,
allowRootService: options.allowRootService,
});
}
function command(
value: unknown,
operation:
| LocalReconciliationApplicationPrepareCommand['operation']
| LocalReconciliationApplicationCommitCommand['operation']
| LocalReconciliationApplicationVerifyCommand['operation'],
): Readonly<{
options: Readonly<LocalReconciliationApplicationOptions>;
request: Record<string, unknown>;
}> {
const selected = object(value, 'reconciliation application command');
exact(
selected,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
configurationError('reconciliation application command is invalid');
}
return Object.freeze({
options: normalizeOptions(selected.options),
request: object(selected.request, 'request'),
});
}
export function normalizeLocalReconciliationApplicationPrepareCommand(
value: unknown,
): Readonly<LocalReconciliationApplicationPrepareCommand> {
const selected = command(
value,
'local.deployment.reconciliation.application.prepare',
);
exact(
selected.request,
[
'applicationId',
'expectedHeadDigest',
'expectedReviewDigest',
'preparedAtMs',
'reviewId',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.preparedAtMs) ||
(selected.request.preparedAtMs as number) < 0
) {
configurationError('reconciliation application prepare command is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.prepare',
options: selected.options,
request: Object.freeze({
applicationId: identifier(
selected.request.applicationId,
'applicationId',
),
reviewId: identifier(selected.request.reviewId, 'reviewId'),
expectedReviewDigest: digest(
selected.request.expectedReviewDigest,
'expectedReviewDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
preparedAtMs: selected.request.preparedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationApplicationCommitCommand(
value: unknown,
): Readonly<LocalReconciliationApplicationCommitCommand> {
const selected = command(
value,
'local.deployment.reconciliation.application.commit',
);
exact(
selected.request,
[
'applicationId',
'committedAtMs',
'expectedHeadDigest',
'expectedPreparationDigest',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.committedAtMs) ||
(selected.request.committedAtMs as number) < 0
) {
configurationError('reconciliation application commit command is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.commit',
options: selected.options,
request: Object.freeze({
applicationId: identifier(
selected.request.applicationId,
'applicationId',
),
expectedPreparationDigest: digest(
selected.request.expectedPreparationDigest,
'expectedPreparationDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
committedAtMs: selected.request.committedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationApplicationVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationApplicationVerifyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.application.verify',
);
exact(
selected.request,
['applicationId', 'expectedApplicationPlanDigest'],
'request',
);
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.verify',
options: selected.options,
request: Object.freeze({
applicationId: identifier(
selected.request.applicationId,
'applicationId',
),
expectedApplicationPlanDigest: digest(
selected.request.expectedApplicationPlanDigest,
'expectedApplicationPlanDigest',
),
}),
});
}
@@ -0,0 +1,510 @@
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import { cutoverDigest } from '../../cutover/targetEvidence';
import type {
LocalReconciliationReviewAuthorizationDomainDecisionCounts,
LocalReconciliationReviewAuthorizationEvidence,
} from '../review/authorization';
import {
LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS,
type LocalReconciliationReviewDisposition,
} from '../review/decisionFile';
import {
LOCAL_RECONCILIATION_PLAN_DOMAINS,
type LocalReconciliationPlanDomain,
} from '../planning/contract';
import type { LocalReconciliationApplicationIntent } from './coordinator';
const PLAN_SCHEMA = 'qinglong3-local-reconciliation-application-plan';
const RECEIPT_SCHEMA =
'qinglong3-local-reconciliation-application-plan-receipt';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const MAX_PLAN_BYTES = 64 * 1024;
export type LocalReconciliationApplicationDomainAction =
| 'no_effect'
| 'adapter_required'
| 'manual_external'
| 'adapter_and_manual';
export interface LocalReconciliationApplicationDatabaseDecisionSummary {
readonly decisionCount: number;
readonly dispositionCounts: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
}
export interface LocalReconciliationApplicationDomainSummary {
readonly domain: LocalReconciliationPlanDomain;
readonly legacy: Readonly<LocalReconciliationApplicationDatabaseDecisionSummary>;
readonly target: Readonly<LocalReconciliationApplicationDatabaseDecisionSummary>;
readonly action: LocalReconciliationApplicationDomainAction;
readonly summaryDigest: string;
}
export interface LocalReconciliationApplicationPlan {
readonly schema: typeof PLAN_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_application_planned';
readonly applicationId: string;
readonly reviewId: string;
readonly profile: 'edge' | 'standalone';
readonly preparationDigest: string;
readonly preparedHeadDigest: string;
readonly reviewDigest: string;
readonly authorizationDigest: string;
readonly decisionSetDigest: string;
readonly decisionCount: number;
readonly committedAtMs: number;
readonly domains: readonly Readonly<LocalReconciliationApplicationDomainSummary>[];
readonly outcome:
| 'no_effect_ready'
| 'adapter_required'
| 'manual_required'
| 'adapter_and_manual_required';
readonly applicationPlanDigest: string;
}
export interface LocalReconciliationApplicationPlanReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_application_planned';
readonly applicationId: string;
readonly reviewId: string;
readonly preparationDigest: string;
readonly preparedHeadDigest: string;
readonly reviewDigest: string;
readonly authorizationDigest: string;
readonly decisionSetDigest: string;
readonly decisionCount: number;
readonly applicationPlanDigest: string;
readonly outcome: LocalReconciliationApplicationPlan['outcome'];
readonly domainCount: 8;
readonly committedAtMs: number;
readonly receiptDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(message);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
configurationError(`${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])
) {
configurationError(`${label} shape is invalid`);
}
}
function safeCount(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function counts(
value: unknown,
label: string,
): Readonly<Record<LocalReconciliationReviewDisposition, number>> {
const selected = object(value, label);
exact(selected, LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS, label);
if (
LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS.some(
(disposition) => !safeCount(selected[disposition]),
)
) {
configurationError(`${label} is invalid`);
}
return Object.freeze(selected) as Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
}
function sumCounts(
selected: Readonly<Record<LocalReconciliationReviewDisposition, number>>,
): number {
const total = LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS.reduce(
(sum, disposition) => sum + BigInt(selected[disposition]),
0n,
);
if (total > BigInt(Number.MAX_SAFE_INTEGER)) {
configurationError('application decision count overflowed');
}
return Number(total);
}
function databaseSummary(
selected: Readonly<Record<LocalReconciliationReviewDisposition, number>>,
): Readonly<LocalReconciliationApplicationDatabaseDecisionSummary> {
const dispositionCounts = Object.freeze({ ...selected });
return Object.freeze({
decisionCount: sumCounts(dispositionCounts),
dispositionCounts,
});
}
function action(
selected: Readonly<LocalReconciliationReviewAuthorizationDomainDecisionCounts>,
): LocalReconciliationApplicationDomainAction {
const adapter =
selected.legacy.adopt_legacy +
selected.legacy.retain_both +
selected.target.retain_both >
0;
const manual =
selected.legacy.defer +
selected.legacy.manual_external +
selected.target.defer +
selected.target.manual_external >
0;
if (adapter && manual) return 'adapter_and_manual';
if (adapter) return 'adapter_required';
if (manual) return 'manual_external';
return 'no_effect';
}
function outcome(
domains: readonly Readonly<LocalReconciliationApplicationDomainSummary>[],
): LocalReconciliationApplicationPlan['outcome'] {
const adapter = domains.some(
(domain) =>
domain.action === 'adapter_required' ||
domain.action === 'adapter_and_manual',
);
const manual = domains.some(
(domain) =>
domain.action === 'manual_external' ||
domain.action === 'adapter_and_manual',
);
if (adapter && manual) return 'adapter_and_manual_required';
if (adapter) return 'adapter_required';
if (manual) return 'manual_required';
return 'no_effect_ready';
}
export function buildLocalReconciliationApplicationPlan(
intent: Readonly<LocalReconciliationApplicationIntent>,
authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>,
committedAtMs: number,
preparedHeadDigest: string,
): Readonly<LocalReconciliationApplicationPlan> {
if (
authorization.domainDecisionCounts.length !== 8 ||
authorization.decisionCount !==
authorization.domainDecisionCounts.reduce(
(total, selected) =>
total + sumCounts(selected.legacy) + sumCounts(selected.target),
0,
)
) {
configurationError('authorization domain decision summary drifted');
}
const domains = Object.freeze(
LOCAL_RECONCILIATION_PLAN_DOMAINS.map((domain, index) => {
const selected = authorization.domainDecisionCounts[index];
if (!selected || selected.domain !== domain) {
configurationError('authorization domain ordering drifted');
}
const legacy = databaseSummary(selected.legacy);
const target = databaseSummary(selected.target);
const selectedAction = action(selected);
const summaryPayload = Object.freeze({
domain,
legacy,
target,
action: selectedAction,
});
return Object.freeze({
...summaryPayload,
summaryDigest: cutoverDigest(summaryPayload),
});
}),
);
const payload = Object.freeze({
schema: PLAN_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_application_planned' as const,
applicationId: intent.command.request.applicationId,
reviewId: intent.command.request.reviewId,
profile: intent.profile,
preparationDigest: intent.preparationDigest,
preparedHeadDigest,
reviewDigest: intent.reviewDigest,
authorizationDigest: intent.authorizationDigest,
decisionSetDigest: intent.decisionSetDigest,
decisionCount: intent.decisionCount,
committedAtMs,
domains,
outcome: outcome(domains),
});
const plan = Object.freeze({
...payload,
applicationPlanDigest: cutoverDigest(payload),
});
if (
Buffer.byteLength(`${JSON.stringify(plan, null, 2)}\n`, 'utf8') >
MAX_PLAN_BYTES
) {
configurationError('reconciliation application plan exceeds 64 KiB');
}
return plan;
}
function normalizeDatabaseSummary(
value: unknown,
label: string,
): Readonly<LocalReconciliationApplicationDatabaseDecisionSummary> {
const selected = object(value, label);
exact(selected, ['decisionCount', 'dispositionCounts'], label);
const dispositionCounts = counts(
selected.dispositionCounts,
`${label} disposition counts`,
);
if (
!safeCount(selected.decisionCount) ||
selected.decisionCount !== sumCounts(dispositionCounts)
) {
configurationError(`${label} aggregate drifted`);
}
return Object.freeze({
decisionCount: selected.decisionCount,
dispositionCounts,
}) as Readonly<LocalReconciliationApplicationDatabaseDecisionSummary>;
}
function normalizeDomain(
value: unknown,
expectedDomain: LocalReconciliationPlanDomain,
): Readonly<LocalReconciliationApplicationDomainSummary> {
const selected = object(value, 'reconciliation application domain');
exact(
selected,
['action', 'domain', 'legacy', 'summaryDigest', 'target'],
'reconciliation application domain',
);
const legacy = normalizeDatabaseSummary(selected.legacy, 'legacy summary');
const target = normalizeDatabaseSummary(selected.target, 'target summary');
const summaryPayload = Object.freeze({
domain: selected.domain,
legacy,
target,
action: selected.action,
});
if (
selected.domain !== expectedDomain ||
![
'no_effect',
'adapter_required',
'manual_external',
'adapter_and_manual',
].includes(selected.action as string) ||
typeof selected.summaryDigest !== 'string' ||
!DIGEST_PATTERN.test(selected.summaryDigest) ||
selected.summaryDigest !== cutoverDigest(summaryPayload)
) {
configurationError('reconciliation application domain drifted');
}
const derivedAction = action({
domain: expectedDomain,
legacy: legacy.dispositionCounts,
target: target.dispositionCounts,
});
if (selected.action !== derivedAction) {
configurationError('reconciliation application domain action drifted');
}
return Object.freeze({
domain: expectedDomain,
legacy,
target,
action: selected.action,
summaryDigest: selected.summaryDigest,
}) as Readonly<LocalReconciliationApplicationDomainSummary>;
}
export function normalizeLocalReconciliationApplicationPlan(
value: unknown,
): Readonly<LocalReconciliationApplicationPlan> {
const plan = object(value, 'reconciliation application plan');
exact(
plan,
[
'applicationId',
'applicationPlanDigest',
'authorizationDigest',
'committedAtMs',
'decisionCount',
'decisionSetDigest',
'domains',
'outcome',
'preparationDigest',
'preparedHeadDigest',
'profile',
'reviewDigest',
'reviewId',
'schema',
'schemaVersion',
'state',
],
'reconciliation application plan',
);
if (!Array.isArray(plan.domains) || plan.domains.length !== 8) {
configurationError('reconciliation application domains are invalid');
}
const domains = Object.freeze(
LOCAL_RECONCILIATION_PLAN_DOMAINS.map((domain, index) =>
normalizeDomain((plan.domains as unknown[])[index], domain),
),
);
const { applicationPlanDigest, ...rawPayload } = plan;
const payload = Object.freeze({ ...rawPayload, domains });
const domainDecisionCount = domains.reduce(
(total, domain) =>
total + domain.legacy.decisionCount + domain.target.decisionCount,
0,
);
if (
plan.schema !== PLAN_SCHEMA ||
plan.schemaVersion !== 1 ||
plan.state !== 'reconciliation_application_planned' ||
typeof plan.applicationId !== 'string' ||
!UUID_V4_PATTERN.test(plan.applicationId) ||
typeof plan.reviewId !== 'string' ||
!UUID_V4_PATTERN.test(plan.reviewId) ||
(plan.profile !== 'edge' && plan.profile !== 'standalone') ||
!safeCount(plan.decisionCount) ||
plan.decisionCount !== domainDecisionCount ||
!Number.isSafeInteger(plan.committedAtMs) ||
(plan.committedAtMs as number) < 0 ||
![
'no_effect_ready',
'adapter_required',
'manual_required',
'adapter_and_manual_required',
].includes(plan.outcome as string) ||
plan.outcome !== outcome(domains) ||
[
plan.preparationDigest,
plan.preparedHeadDigest,
plan.reviewDigest,
plan.authorizationDigest,
plan.decisionSetDigest,
applicationPlanDigest,
].some(
(candidate) =>
typeof candidate !== 'string' || !DIGEST_PATTERN.test(candidate),
) ||
cutoverDigest(payload) !== applicationPlanDigest
) {
configurationError('reconciliation application plan drifted');
}
return Object.freeze({
...(plan as unknown as LocalReconciliationApplicationPlan),
domains,
});
}
export function buildLocalReconciliationApplicationPlanReceipt(
plan: Readonly<LocalReconciliationApplicationPlan>,
): Readonly<LocalReconciliationApplicationPlanReceipt> {
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_application_planned' as const,
applicationId: plan.applicationId,
reviewId: plan.reviewId,
preparationDigest: plan.preparationDigest,
preparedHeadDigest: plan.preparedHeadDigest,
reviewDigest: plan.reviewDigest,
authorizationDigest: plan.authorizationDigest,
decisionSetDigest: plan.decisionSetDigest,
decisionCount: plan.decisionCount,
applicationPlanDigest: plan.applicationPlanDigest,
outcome: plan.outcome,
domainCount: 8 as const,
committedAtMs: plan.committedAtMs,
});
return Object.freeze({ ...payload, receiptDigest: cutoverDigest(payload) });
}
export function normalizeLocalReconciliationApplicationPlanReceipt(
value: unknown,
): Readonly<LocalReconciliationApplicationPlanReceipt> {
const receipt = object(value, 'reconciliation application receipt');
exact(
receipt,
[
'applicationId',
'applicationPlanDigest',
'authorizationDigest',
'committedAtMs',
'decisionCount',
'decisionSetDigest',
'domainCount',
'outcome',
'preparationDigest',
'preparedHeadDigest',
'receiptDigest',
'reviewDigest',
'reviewId',
'schema',
'schemaVersion',
'state',
],
'reconciliation application receipt',
);
const { receiptDigest, ...payload } = receipt;
if (
receipt.schema !== RECEIPT_SCHEMA ||
receipt.schemaVersion !== 1 ||
receipt.state !== 'reconciliation_application_planned' ||
typeof receipt.applicationId !== 'string' ||
!UUID_V4_PATTERN.test(receipt.applicationId) ||
typeof receipt.reviewId !== 'string' ||
!UUID_V4_PATTERN.test(receipt.reviewId) ||
!safeCount(receipt.decisionCount) ||
receipt.domainCount !== 8 ||
!Number.isSafeInteger(receipt.committedAtMs) ||
(receipt.committedAtMs as number) < 0 ||
![
'no_effect_ready',
'adapter_required',
'manual_required',
'adapter_and_manual_required',
].includes(receipt.outcome as string) ||
[
receipt.preparationDigest,
receipt.preparedHeadDigest,
receipt.reviewDigest,
receipt.authorizationDigest,
receipt.decisionSetDigest,
receipt.applicationPlanDigest,
receiptDigest,
].some(
(candidate) =>
typeof candidate !== 'string' || !DIGEST_PATTERN.test(candidate),
) ||
cutoverDigest(payload) !== receiptDigest
) {
configurationError('reconciliation application receipt drifted');
}
return receipt as unknown as Readonly<LocalReconciliationApplicationPlanReceipt>;
}
@@ -9,6 +9,10 @@ import {
} from '@qinglong/runtime-core/security';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import {
LOCAL_RECONCILIATION_PLAN_DOMAINS,
type LocalReconciliationPlanDomain,
} from '../planning/contract';
import type {
LocalReconciliationReviewDecision,
LocalReconciliationReviewDisposition,
@@ -54,9 +58,20 @@ export interface LocalReconciliationReviewAuthorizationEvidence {
readonly reasonCounts: Readonly<
Record<LocalReconciliationReviewReason, number>
>;
readonly domainDecisionCounts: readonly Readonly<LocalReconciliationReviewAuthorizationDomainDecisionCounts>[];
readonly header: Readonly<LocalReconciliationReviewAuthorizationHeader>;
}
export interface LocalReconciliationReviewAuthorizationDomainDecisionCounts {
readonly domain: LocalReconciliationPlanDomain;
readonly legacy: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
readonly target: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
}
interface SignatureRecord {
readonly schemaVersion: 1;
readonly kind: typeof SIGNATURE_KIND;
@@ -97,6 +112,32 @@ function zeroCounts<T extends string>(keys: readonly T[]): Record<T, number> {
return Object.fromEntries(keys.map((key) => [key, 0])) as Record<T, number>;
}
function zeroDomainDecisionCounts(): Array<{
domain: LocalReconciliationPlanDomain;
legacy: Record<LocalReconciliationReviewDisposition, number>;
target: Record<LocalReconciliationReviewDisposition, number>;
}> {
return LOCAL_RECONCILIATION_PLAN_DOMAINS.map((domain) => ({
domain,
legacy: zeroCounts(DISPOSITIONS),
target: zeroCounts(DISPOSITIONS),
}));
}
function freezeDomainDecisionCounts(
counts: ReturnType<typeof zeroDomainDecisionCounts>,
): readonly Readonly<LocalReconciliationReviewAuthorizationDomainDecisionCounts>[] {
return Object.freeze(
counts.map((selected) =>
Object.freeze({
domain: selected.domain,
legacy: Object.freeze({ ...selected.legacy }),
target: Object.freeze({ ...selected.target }),
}),
),
);
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation review authorization ${message}`,
@@ -429,6 +470,7 @@ export async function publishLocalReconciliationReviewAuthorization(
let decisionCount = 0;
const dispositionCounts = zeroCounts(DISPOSITIONS);
const reasonCounts = zeroCounts(REASONS);
const domainDecisionCounts = zeroDomainDecisionCounts();
const writeContent = (value: unknown, isDecision: boolean): void => {
const line = canonicalLine(value);
try {
@@ -452,6 +494,11 @@ export async function publishLocalReconciliationReviewAuthorization(
decisionCount += 1;
dispositionCounts[selected.disposition] += 1;
reasonCounts[selected.reason] += 1;
const domain = domainDecisionCounts.find(
(candidate) => candidate.domain === selected.domain,
);
if (!domain) configurationError('decision domain is unavailable');
domain[selected.database][selected.disposition] += 1;
});
if (!DIGEST_PATTERN.test(decisionFile.decisionFileDigest)) {
configurationError('decision file evidence is invalid');
@@ -532,6 +579,7 @@ export async function publishLocalReconciliationReviewAuthorization(
keyId: material.keyId,
dispositionCounts: Object.freeze(dispositionCounts),
reasonCounts: Object.freeze(reasonCounts),
domainDecisionCounts: freezeDomainDecisionCounts(domainDecisionCounts),
header: options.header,
});
} catch (error) {
@@ -668,6 +716,7 @@ export async function verifyLocalReconciliationReviewAuthorization(
}
const dispositionCounts = zeroCounts(DISPOSITIONS);
const reasonCounts = zeroCounts(REASONS);
const domainDecisionCounts = zeroDomainDecisionCounts();
let decisionCount = 0;
let pending = nextLine();
if (pending === null) configurationError('signature is absent');
@@ -679,6 +728,11 @@ export async function verifyLocalReconciliationReviewAuthorization(
decisionCount += 1;
dispositionCounts[selected.disposition] += 1;
reasonCounts[selected.reason] += 1;
const domain = domainDecisionCounts.find(
(candidate) => candidate.domain === selected.domain,
);
if (!domain) configurationError('decision domain is unavailable');
domain[selected.database][selected.disposition] += 1;
pending.fill(0);
pending = following;
}
@@ -740,6 +794,7 @@ export async function verifyLocalReconciliationReviewAuthorization(
keyId: signature.keyId,
dispositionCounts: Object.freeze(dispositionCounts),
reasonCounts: Object.freeze(reasonCounts),
domainDecisionCounts: freezeDomainDecisionCounts(domainDecisionCounts),
header: normalizedHeader,
});
} catch (error) {
@@ -504,6 +504,50 @@ async function readTerminal(
return Object.freeze({ authorization, review, receipt });
}
export interface LocalReconciliationReviewTerminal {
readonly intent: Readonly<LocalReconciliationReviewIntent>;
readonly authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
readonly review: Readonly<LocalReconciliationReview>;
readonly receipt: Readonly<LocalReconciliationReviewReceipt>;
}
export async function readLocalReconciliationReviewTerminal(
reviewRoot: string,
reviewId: string,
issuerKeyringPath: string,
uid: number,
): Promise<Readonly<LocalReconciliationReviewTerminal>> {
const selected = paths(reviewRoot, reviewId);
validateDirectory(
selected.root,
uid,
[0o500],
'reconciliationReviewDirectory',
);
validateDirectory(
selected.staging,
uid,
[0o500],
'reconciliationReviewStaging',
);
validateCatalog(selected, true);
const intent = readIntent(selected, uid);
if (
intent.command.options.reviewRoot !== reviewRoot ||
intent.command.request.reviewId !== reviewId
) {
configurationError('terminal reconciliation review path binding drifted');
}
const terminal = await readTerminal(
selected,
intent,
issuerKeyringPath,
uid,
[0o400],
);
return Object.freeze({ intent, ...terminal });
}
function result(
operation: LocalReconciliationReviewTerminalResult['operation'],
status: LocalReconciliationReviewTerminalResult['status'],
@@ -286,6 +286,10 @@ export function assertLocalReconciliationReviewDecisionMatchesFact(
}
if (
fact.decisionRequirement === 'informational' ||
(fact.database === 'legacy' && selected.disposition === 'retain_target') ||
(fact.database === 'target' &&
(selected.disposition === 'adopt_legacy' ||
selected.disposition === 'exclude_legacy')) ||
(fact.decisionRequirement === 'blocked' &&
selected.disposition !== 'defer' &&
selected.disposition !== 'manual_external') ||
@@ -753,7 +753,9 @@ function replayResult(
head.state === 'reconciliation_plan_prepared' ||
head.state === 'reconciliation_planned' ||
head.state === 'reconciliation_review_prepared' ||
head.state === 'reconciliation_reviewed');
head.state === 'reconciliation_reviewed' ||
head.state === 'reconciliation_application_prepared' ||
head.state === 'reconciliation_application_planned');
if (
record.actionId !== intent.actionId ||
record.intentDigest !== intent.intentDigest ||
@@ -9,12 +9,15 @@ const { test } = require('node:test');
const {
commitLocalReconciliationCapture,
commitLocalReconciliationApplication,
commitLocalReconciliationPlan,
commitLocalReconciliationReview,
prepareLocalReconciliationCapture,
prepareLocalReconciliationApplication,
prepareLocalReconciliationPlan,
prepareLocalReconciliationReview,
verifyLocalReconciliationCapture,
verifyLocalReconciliationApplication,
verifyLocalReconciliationPlan,
verifyLocalReconciliationReview,
writeLocalReconciliationReviewDiagnostics,
@@ -799,6 +802,7 @@ function writeReviewDecisionFile(state, prepared, fileName = 'review.ndjson') {
for (const fact of page.records) {
if (fact.decisionRequirement === 'informational') continue;
const blocked = fact.decisionRequirement === 'blocked';
const legacy = fact.database === 'legacy';
decisions.push({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-review-decision',
@@ -807,9 +811,15 @@ function writeReviewDecisionFile(state, prepared, fileName = 'review.ndjson') {
factKind: fact.factKind,
ordinal: fact.ordinal,
factDigest: fact.factDigest,
disposition: blocked ? 'manual_external' : 'retain_target',
disposition: blocked
? 'manual_external'
: legacy
? 'exclude_legacy'
: 'retain_target',
reason: blocked
? 'external_recovery_required'
: legacy
? 'legacy_excluded'
: 'preserve_target',
});
}
@@ -924,6 +934,67 @@ function reviewCommitFixture(t, options = {}) {
};
}
async function reviewedApplicationFixture(t, options = {}) {
const state = reviewCommitFixture(t, options);
options.mutateDecisions?.(state.reviewFile.records);
if (options.mutateDecisions) {
fs.writeFileSync(
state.reviewFile.filePath,
`${state.reviewFile.records
.map((record) => JSON.stringify(record))
.join('\n')}\n`,
{ mode: 0o600 },
);
}
const reviewed = await commitLocalReconciliationReview(
state.command,
state.dependencies,
);
const applicationRoot = path.join(
path.dirname(state.captureRoot),
`application-root-${options.reviewSuffix ?? '1'}`,
);
fs.mkdirSync(applicationRoot, { mode: 0o700 });
const applicationId =
options.applicationId ?? '00000000-0000-4000-8000-000000000401';
const prepareCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.prepare',
options: {
...state.reviewCommand.options,
applicationRoot,
issuerKeyringPath: state.issuerKeyringPath,
},
request: {
applicationId,
reviewId: state.reviewCommand.request.reviewId,
expectedReviewDigest: reviewed.reviewDigest,
expectedHeadDigest: reviewed.instanceHeadDigest,
preparedAtMs: state.command.request.committedAtMs + 1,
},
};
return {
...state,
reviewed,
applicationRoot,
prepareApplicationCommand: prepareCommand,
};
}
function applicationCommitCommand(state, prepared) {
return {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.commit',
options: state.prepareApplicationCommand.options,
request: {
applicationId: state.prepareApplicationCommand.request.applicationId,
expectedPreparationDigest: prepared.preparationDigest,
expectedHeadDigest: prepared.instanceHeadDigest,
committedAtMs: state.prepareApplicationCommand.request.preparedAtMs + 1,
},
};
}
function dockerReadSealedSqlite(assetsDirectory, mode) {
const source =
mode === 'main_only_immutable'
@@ -2157,6 +2228,35 @@ test('review commit rejects missing and policy-invalid decisions before terminal
commitLocalReconciliationReview(blocked.command, blocked.dependencies),
/not allowed for canonical fact/,
);
const crossDatabase = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000390',
reviewId: '00000000-0000-4000-8000-000000000391',
reviewSuffix: 'cross-database',
});
const legacyDecision = crossDatabase.reviewFile.records.find(
(record) =>
record.kind === 'qinglong3-local-reconciliation-review-decision' &&
record.database === 'legacy' &&
record.disposition === 'exclude_legacy',
);
assert.ok(legacyDecision);
legacyDecision.disposition = 'retain_target';
legacyDecision.reason = 'preserve_target';
fs.writeFileSync(
crossDatabase.reviewFile.filePath,
`${crossDatabase.reviewFile.records
.map((record) => JSON.stringify(record))
.join('\n')}\n`,
{ mode: 0o600 },
);
await assert.rejects(
commitLocalReconciliationReview(
crossDatabase.command,
crossDatabase.dependencies,
),
/not allowed for canonical fact/,
);
});
test('review commit rejects weak principals, oversized Edge streams and decision-file drift around signing', async (t) => {
@@ -2329,6 +2429,262 @@ test('review commit resumes authorization, receipt, seal and head response-loss
assert.equal(headReplay.status, 'existing');
});
test('application coordinator plans eight content-free domains and verifies without SQLite writes', async (t) => {
const state = await reviewedApplicationFixture(t, {
planId: '00000000-0000-4000-8000-000000000401',
reviewId: '00000000-0000-4000-8000-000000000402',
applicationId: '00000000-0000-4000-8000-000000000403',
reviewSuffix: 'application-terminal',
mutateDecisions(records) {
const selected = records.find(
(record) =>
record.kind === 'qinglong3-local-reconciliation-review-decision' &&
record.database === 'legacy' &&
record.domain === 'automation' &&
record.disposition === 'exclude_legacy',
);
assert.ok(selected);
selected.disposition = 'adopt_legacy';
selected.reason = 'prefer_legacy';
},
});
const targetBefore = fs.statSync(state.targetDatabasePath, { bigint: true });
const targetBytes = fs.readFileSync(state.targetDatabasePath);
const prepared = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.state, 'reconciliation_application_prepared');
const commitCommand = applicationCommitCommand(state, prepared);
const committed = await commitLocalReconciliationApplication(commitCommand);
assert.equal(committed.status, 'prepared');
assert.equal(committed.state, 'reconciliation_application_planned');
assert.equal(committed.domainCount, 8);
assert.equal(committed.outcome, 'adapter_and_manual_required');
const applicationDirectory = path.join(
state.applicationRoot,
state.prepareApplicationCommand.request.applicationId,
);
assert.deepEqual(fs.readdirSync(applicationDirectory).sort(), [
'intent.json',
'plan.json',
'receipt.json',
'staging',
]);
assert.equal(fs.statSync(applicationDirectory).mode & 0o777, 0o500);
assert.equal(
fs.statSync(path.join(applicationDirectory, 'staging')).mode & 0o777,
0o500,
);
for (const fileName of ['intent.json', 'plan.json', 'receipt.json']) {
assert.equal(
fs.statSync(path.join(applicationDirectory, fileName)).mode & 0o777,
0o400,
);
}
const planText = fs.readFileSync(
path.join(applicationDirectory, 'plan.json'),
'utf8',
);
const plan = JSON.parse(planText);
assert.equal(plan.domains.length, 8);
assert.equal(
plan.domains.find((domain) => domain.domain === 'automation').action,
'adapter_required',
);
assert.equal(planText.includes('QingLong3TaskDefinitions'), false);
assert.equal(planText.includes('Crontabs'), false);
assert.equal(planText.includes('private-value'), false);
assert.equal(planText.includes('0 0 * * *'), false);
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.verify',
options: state.prepareApplicationCommand.options,
request: {
applicationId: state.prepareApplicationCommand.request.applicationId,
expectedApplicationPlanDigest: committed.applicationPlanDigest,
},
};
const verified = await verifyLocalReconciliationApplication(verifyCommand);
assert.equal(verified.status, 'verified');
assert.equal(verified.applicationPlanDigest, committed.applicationPlanDigest);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_application_planned');
assert.equal(head.sourceRecordDigest, committed.applicationPlanDigest);
const targetAfter = fs.statSync(state.targetDatabasePath, { bigint: true });
assert.equal(
fs.readFileSync(state.targetDatabasePath).equals(targetBytes),
true,
);
assert.equal(targetAfter.mtimeNs, targetBefore.mtimeNs);
assert.equal(targetAfter.ctimeNs, targetBefore.ctimeNs);
const commandPath = path.join(
state.deploymentRoot,
'application-verify.json',
);
fs.writeFileSync(commandPath, `${JSON.stringify(verifyCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-application-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(state.applicationRoot), false);
assert.equal(cli.stdout.includes('review-owner'), false);
assert.equal(cli.stdout.includes('Crontabs'), false);
});
test('application coordinator resumes every publication window and fences competitors', async (t) => {
const prepareCrash = await reviewedApplicationFixture(t, {
planId: '00000000-0000-4000-8000-000000000410',
reviewId: '00000000-0000-4000-8000-000000000411',
applicationId: '00000000-0000-4000-8000-000000000412',
reviewSuffix: 'application-prepare-crash',
});
await assert.rejects(
prepareLocalReconciliationApplication(
prepareCrash.prepareApplicationCommand,
{
afterHeadPrepared() {
throw new Error('application prepare head response loss');
},
},
),
/application prepare head response loss/,
);
const preparedReplay = await prepareLocalReconciliationApplication(
prepareCrash.prepareApplicationCommand,
);
assert.equal(preparedReplay.state, 'reconciliation_application_prepared');
await assert.rejects(
prepareLocalReconciliationApplication({
...prepareCrash.prepareApplicationCommand,
request: {
...prepareCrash.prepareApplicationCommand.request,
applicationId: '00000000-0000-4000-8000-00000000041f',
},
}),
/lost reviewed head compare-and-swap/,
);
const windows = [
['plan', '000000000413', '000000000414', '000000000415'],
['receipt', '000000000416', '000000000417', '000000000418'],
['seal', '000000000419', '00000000041a', '00000000041b'],
['head', '00000000041c', '00000000041d', '00000000041e'],
];
for (const [window, planTail, reviewTail, applicationTail] of windows) {
const state = await reviewedApplicationFixture(t, {
planId: `00000000-0000-4000-8000-${planTail}`,
reviewId: `00000000-0000-4000-8000-${reviewTail}`,
applicationId: `00000000-0000-4000-8000-${applicationTail}`,
reviewSuffix: `application-${window}-crash`,
});
const prepared = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
const commitCommand = applicationCommitCommand(state, prepared);
const callback =
window === 'plan'
? 'afterPlanPublished'
: window === 'receipt'
? 'afterReceiptPublished'
: window === 'seal'
? 'afterTerminalSealed'
: 'afterHeadAdvanced';
await assert.rejects(
commitLocalReconciliationApplication(commitCommand, {
[callback]() {
throw new Error(`application ${window} response loss`);
},
}),
new RegExp(`application ${window} response loss`),
);
const replay = await commitLocalReconciliationApplication(commitCommand);
assert.equal(replay.state, 'reconciliation_application_planned');
if (window === 'head') assert.equal(replay.status, 'existing');
}
});
test('application verify rejects digest and head drift without repairing state', async (t) => {
const state = await reviewedApplicationFixture(t, {
planId: '00000000-0000-4000-8000-000000000421',
reviewId: '00000000-0000-4000-8000-000000000422',
applicationId: '00000000-0000-4000-8000-000000000423',
reviewSuffix: 'application-verify-drift',
});
const prepared = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
const committed = await commitLocalReconciliationApplication(
applicationCommitCommand(state, prepared),
);
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.application.verify',
options: state.prepareApplicationCommand.options,
request: {
applicationId: state.prepareApplicationCommand.request.applicationId,
expectedApplicationPlanDigest: 'f'.repeat(64),
},
};
const applicationDirectory = path.join(
state.applicationRoot,
state.prepareApplicationCommand.request.applicationId,
);
const before = fs.statSync(path.join(applicationDirectory, 'plan.json'), {
bigint: true,
});
await assert.rejects(
verifyLocalReconciliationApplication(verifyCommand),
/expected digest drifted/,
);
const after = fs.statSync(path.join(applicationDirectory, 'plan.json'), {
bigint: true,
});
assert.equal(after.mtimeNs, before.mtimeNs);
assert.equal(after.ctimeNs, before.ctimeNs);
assert.notEqual(committed.applicationPlanDigest, 'f'.repeat(64));
const headPath = path.join(
state.deploymentRoot,
'service',
'cutover-instances',
state.captureCommand.request.instanceId,
'head.json',
);
const head = JSON.parse(fs.readFileSync(headPath, 'utf8'));
delete head.headDigest;
head.sourceRecordDigest = 'e'.repeat(64);
head.headDigest = digest(head);
fs.writeFileSync(headPath, `${JSON.stringify(head, null, 2)}\n`, {
mode: 0o600,
});
await assert.rejects(
verifyLocalReconciliationApplication({
...verifyCommand,
request: {
...verifyCommand.request,
expectedApplicationPlanDigest: committed.applicationPlanDigest,
},
}),
/lost terminal instance head/,
);
});
test(
'real stopped Docker target produces an independently verified bundle',
{ skip: process.env.QL3_RECONCILIATION_DOCKER_GATE !== '1' },