feat(ql3): prepare private reconciliation reviews

This commit is contained in:
whyour
2026-08-21 18:35:28 +08:00
parent 473a535815
commit 827ba5294f
13 changed files with 1744 additions and 8 deletions
+7 -4
View File
@@ -11,9 +11,9 @@
最新增量证据(2026-08-21): 最新增量证据(2026-08-21):
- D-391/ADR-0484契约已冻结,待实现):`reconciliation_planned` 仍是内容无关 summary,不能为人工裁决泄漏明细或获得 import authority。 - D-391/ADR-0484第一切片已实现,commit/verify 待实现):`reconciliation_planned` 仍是内容无关 summary,不能为人工裁决泄漏明细或
下一切片在既有 Local Owner `deployment/reconciliation/review/` 增加 获得 import authority。既有 Local Owner 已在 `deployment/reconciliation/review/` 增加
`reconciliation.review.prepare|diagnostics|commit|verify`,以 `reconciliation.review.prepare|diagnostics`,并为后续 commit/verify 冻结
`reconciliation_planned → reconciliation_review_prepared → reconciliation_reviewed` CAS 建立唯一 review fence。diagnostics 每次只把 `reconciliation_planned → reconciliation_review_prepared → reconciliation_reviewed` CAS 建立唯一 review fence。diagnostics 每次只把
一个 database/domain/fact-kind 的最多 64 条私有记录 no-replace 写入 caller 指定的 owner-only 文件;stdout 只返回 page digest、计数和 一个 database/domain/fact-kind 的最多 64 条私有记录 no-replace 写入 caller 指定的 owner-only 文件;stdout 只返回 page digest、计数和
offset,不含路径、名称或 fact digest。commit 不信任 page,而是从 exact sealed bundle 重新流式派生 canonical facts,与 Edge ≤8 MiB、 offset,不含路径、名称或 fact digest。commit 不信任 page,而是从 exact sealed bundle 重新流式派生 canonical facts,与 Edge ≤8 MiB、
@@ -22,7 +22,10 @@
exact review file/plan/bundle/head fence 签名;terminal review 只保存签名 authorization、compact counts/digest 和 content-free receipt。 exact review file/plan/bundle/head fence 签名;terminal review 只保存签名 authorization、compact counts/digest 和 content-free receipt。
verify 不打开 SQLite。实现不新增 package/dependency/binary/daemon,不把文件平铺回 `src/` 根,也不调用 DML、Secret 解密、Docker/init/ verify 不打开 SQLite。实现不新增 package/dependency/binary/daemon,不把文件平铺回 `src/` 根,也不调用 DML、Secret 解密、Docker/init/
network。后续领域 adapter 必须消费 exact `reviewDigest` 后重新认证和授权,分别定义 backup、prepare/commit、幂等与 rollback,不能把 network。后续领域 adapter 必须消费 exact `reviewDigest` 后重新认证和授权,分别定义 backup、prepare/commit、幂等与 rollback,不能把
review completion 当作 reconciliation completion。 review completion 当作 reconciliation completion。当前聚焦套件 `28 total / 26 pass / 2 conditional Docker skip / 0 fail`Local Owner
`250 total / 243 pass / 7 conditional skip / 0 fail`tracked backend `1540 total / 1538 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/发布审计、十四档 artifact 和真实 Docker readonly `2/2` 全通过。workspace 仍为 18
packagesLocal Owner `149 source / 148 nested / 1 root binary entry`;新增 3 个源码全部位于 review 子目录,基础常驻 closure 不增长。
- D-390/ADR-0483(已接受):既有 Local Owner 已实现密封 capture 的严格只读消费与独立 - D-390/ADR-0483(已接受):既有 Local Owner 已实现密封 capture 的严格只读消费与独立
`reconciliation.plan.prepare|commit|verify`。capture v2 使用 SQLite 可识别的固定 `target.sqlite* / legacy.sqlite* / `reconciliation.plan.prepare|commit|verify`。capture v2 使用 SQLite 可识别的固定 `target.sqlite* / legacy.sqlite* /
recovery.sqlite` 物理名和 `0400/0500` terminal sealmain-only 走 immutable readonlyWAL+SHM 完整配对走普通 readonlyhot recovery.sqlite` 物理名和 `0400/0500` terminal sealmain-only 走 immutable readonlyWAL+SHM 完整配对走普通 readonlyhot
@@ -119,6 +119,23 @@ import Local SQLite mutation、adoption publisher 或 runtime execution authorit
运行时最多一个 SQLite handle、一个 decision iterator、64 KiB hash/read buffer、64 条 page record 和固定八领域 counters。diagnostics 运行时最多一个 SQLite handle、一个 decision iterator、64 KiB hash/read buffer、64 条 page record 和固定八领域 counters。diagnostics
output 不进入基础 Edge/Standalone artifactCluster/PostgreSQL/Kubernetes 不读取 Local review root、authorization 或 reviewer identity。 output 不进入基础 Edge/Standalone artifactCluster/PostgreSQL/Kubernetes 不读取 Local review root、authorization 或 reviewer identity。
## 当前实现进度
D-391 第一切片已实现 `review.prepare``review.diagnostics`。prepare 读取并重验 exact terminal plan/bundle,以 instance CAS 建立
`reconciliation_review_prepared` 唯一 fence,覆盖 head response loss、第二 review、rollback 和 restart 拒绝。diagnostics 在每次
SQLite open 前后重验密封资产,只为一个 database/domain/fact-kind 发布最多 64 条 owner-only pageSecret、identity、history 和 unknown
facts 固定 blocked,未知表不读取 rowterminal result 不返回路径、名称或 fact digest。page 使用 256 KiB 固定上限、deterministic
stage、hard-link no-replace 与 fsync,重复请求只能得到 byte-exact existing page。
验证结果:聚焦套件 `28 total / 26 pass / 2 conditional Docker skip / 0 fail`;完整 Local Owner
`250 total / 243 pass / 7 conditional skip / 0 fail`tracked backend `1540 total / 1538 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/发布审计、十四档 artifact audit 与真实 Docker readonly `2/2` 均通过。workspace 仍为 18
packages`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner 为 `149 source / 148 nested / 1 root binary entry`,新增
3 个源码全部位于 `deployment/reconciliation/review/`,没有新增 dependency 或常驻对象,基础 Edge/Standalone closure 未增长。
`review.commit`、强认证 User/issuer keyring authorization、terminal seal、`reconciliation_reviewed` 推进和 `review.verify` 尚未实现,因此本
ADR 继续保持 Proposed;当前 prepared review 不授予任何 import、rollback 或 restart authority。
## 被拒绝的替代方案 ## 被拒绝的替代方案
### 把对象名和冲突列表直接加入 plan ### 把对象名和冲突列表直接加入 plan
@@ -27,6 +27,8 @@ export type LocalCutoverInstanceHeadState =
| 'reconciliation_captured' | 'reconciliation_captured'
| 'reconciliation_plan_prepared' | 'reconciliation_plan_prepared'
| 'reconciliation_planned' | 'reconciliation_planned'
| 'reconciliation_review_prepared'
| 'reconciliation_reviewed'
| 'rollback_prepared' | 'rollback_prepared'
| 'legacy_restart_requested' | 'legacy_restart_requested'
| 'legacy_running' | 'legacy_running'
@@ -162,6 +164,8 @@ function parseHead(value: unknown): Readonly<LocalCutoverInstanceHead> {
head.state !== 'reconciliation_captured' && head.state !== 'reconciliation_captured' &&
head.state !== 'reconciliation_plan_prepared' && head.state !== 'reconciliation_plan_prepared' &&
head.state !== 'reconciliation_planned' && head.state !== 'reconciliation_planned' &&
head.state !== 'reconciliation_review_prepared' &&
head.state !== 'reconciliation_reviewed' &&
head.state !== 'rollback_prepared' && head.state !== 'rollback_prepared' &&
head.state !== 'legacy_restart_requested' && head.state !== 'legacy_restart_requested' &&
head.state !== 'legacy_running' && head.state !== 'legacy_running' &&
@@ -332,6 +336,8 @@ export function advanceLocalCutoverInstanceHead(
| 'reconciliation_captured' | 'reconciliation_captured'
| 'reconciliation_plan_prepared' | 'reconciliation_plan_prepared'
| 'reconciliation_planned' | 'reconciliation_planned'
| 'reconciliation_review_prepared'
| 'reconciliation_reviewed'
| 'rollback_prepared' | 'rollback_prepared'
| 'legacy_restart_requested' | 'legacy_restart_requested'
| 'legacy_running' | 'legacy_running'
@@ -374,6 +380,8 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_captured' || current.state === 'reconciliation_captured' ||
current.state === 'reconciliation_plan_prepared' || current.state === 'reconciliation_plan_prepared' ||
current.state === 'reconciliation_planned' || current.state === 'reconciliation_planned' ||
current.state === 'reconciliation_review_prepared' ||
current.state === 'reconciliation_reviewed' ||
current.state === 'legacy_restart_requested' || current.state === 'legacy_restart_requested' ||
current.state === 'legacy_running' || current.state === 'legacy_running' ||
current.state === 'legacy_ready') current.state === 'legacy_ready')
@@ -397,6 +405,10 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_captured') || current.state === 'reconciliation_captured') ||
(state === 'reconciliation_planned' && (state === 'reconciliation_planned' &&
current.state === 'reconciliation_plan_prepared') || current.state === 'reconciliation_plan_prepared') ||
(state === 'reconciliation_review_prepared' &&
current.state === 'reconciliation_planned') ||
(state === 'reconciliation_reviewed' &&
current.state === 'reconciliation_review_prepared') ||
(state === 'rollback_prepared' && current.state === 'target_stopped') || (state === 'rollback_prepared' && current.state === 'target_stopped') ||
(state === 'legacy_restart_requested' && (state === 'legacy_restart_requested' &&
current.state === 'rollback_prepared') || current.state === 'rollback_prepared') ||
@@ -111,6 +111,12 @@ import {
verifyLocalReconciliationPlan, verifyLocalReconciliationPlan,
verifyLocalReconciliationPlanCommandFile, verifyLocalReconciliationPlanCommandFile,
} from './reconciliation/planning/preparation'; } from './reconciliation/planning/preparation';
import {
prepareLocalReconciliationReview,
prepareLocalReconciliationReviewCommandFile,
writeLocalReconciliationReviewDiagnostics,
writeLocalReconciliationReviewDiagnosticsCommandFile,
} from './reconciliation/review/preparation';
export { export {
commitLocalReconciliationPlan, commitLocalReconciliationPlan,
@@ -119,6 +125,10 @@ export {
prepareLocalReconciliationPlanCommandFile, prepareLocalReconciliationPlanCommandFile,
verifyLocalReconciliationPlan, verifyLocalReconciliationPlan,
verifyLocalReconciliationPlanCommandFile, verifyLocalReconciliationPlanCommandFile,
prepareLocalReconciliationReview,
prepareLocalReconciliationReviewCommandFile,
writeLocalReconciliationReviewDiagnostics,
writeLocalReconciliationReviewDiagnosticsCommandFile,
}; };
export { export {
@@ -160,6 +170,30 @@ export {
type LocalReconciliationCaptureVerifyCommand, type LocalReconciliationCaptureVerifyCommand,
type LocalReconciliationStoppedAuthority, type LocalReconciliationStoppedAuthority,
} from './reconciliation/contract'; } from './reconciliation/contract';
export {
LOCAL_RECONCILIATION_DIAGNOSTIC_FACT_KINDS,
normalizeLocalReconciliationReviewDiagnosticsCommand,
normalizeLocalReconciliationReviewPrepareCommand,
type LocalReconciliationDiagnosticFactKind,
type LocalReconciliationReviewDiagnosticsCommand,
type LocalReconciliationReviewDiagnosticsResult,
type LocalReconciliationReviewOptions,
type LocalReconciliationReviewPrepareCommand,
type LocalReconciliationReviewPrepareResult,
} from './reconciliation/review/contract';
export {
localReconciliationReviewDirectory,
normalizeLocalReconciliationReviewIntent,
readLocalReconciliationReviewIntent,
type LocalReconciliationReviewDependencies,
type LocalReconciliationReviewIntent,
} from './reconciliation/review/preparation';
export {
type LocalReconciliationDiagnosticDecisionRequirement,
type LocalReconciliationDiagnosticFact,
type LocalReconciliationDiagnosticPage,
type LocalReconciliationDiagnosticReason,
} from './reconciliation/review/diagnostics';
export { export {
LOCAL_RECONCILIATION_PLAN_DOMAINS, LOCAL_RECONCILIATION_PLAN_DOMAINS,
normalizeLocalReconciliationPlanCommitCommand, normalizeLocalReconciliationPlanCommitCommand,
@@ -15,10 +15,12 @@ import {
prepareLocalServiceManagerLegacyRollbackCommandFile, prepareLocalServiceManagerLegacyRollbackCommandFile,
prepareLocalReconciliationCaptureCommandFile, prepareLocalReconciliationCaptureCommandFile,
prepareLocalReconciliationPlanCommandFile, prepareLocalReconciliationPlanCommandFile,
prepareLocalReconciliationReviewCommandFile,
commitLocalReconciliationCaptureCommandFile, commitLocalReconciliationCaptureCommandFile,
commitLocalReconciliationPlanCommandFile, commitLocalReconciliationPlanCommandFile,
verifyLocalReconciliationCaptureCommandFile, verifyLocalReconciliationCaptureCommandFile,
verifyLocalReconciliationPlanCommandFile, verifyLocalReconciliationPlanCommandFile,
writeLocalReconciliationReviewDiagnosticsCommandFile,
prepareLocalDeploymentCommandFile, prepareLocalDeploymentCommandFile,
proveLocalDeploymentLegacyReadinessCommandFile, proveLocalDeploymentLegacyReadinessCommandFile,
restoreLocalDeploymentComposeCommitCommandFile, restoreLocalDeploymentComposeCommitCommandFile,
@@ -33,7 +35,7 @@ import {
} from './localDeployment'; } from './localDeployment';
const USAGE = 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|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|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> { async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) { if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -68,6 +70,8 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-plan-prepare' && argv[0] !== 'reconciliation-plan-prepare' &&
argv[0] !== 'reconciliation-plan-commit' && argv[0] !== 'reconciliation-plan-commit' &&
argv[0] !== 'reconciliation-plan-verify' && argv[0] !== 'reconciliation-plan-verify' &&
argv[0] !== 'reconciliation-review-prepare' &&
argv[0] !== 'reconciliation-review-diagnostics' &&
argv[0] !== 'compose-revision' && argv[0] !== 'compose-revision' &&
argv[0] !== 'compose-preflight' && argv[0] !== 'compose-preflight' &&
argv[0] !== 'compose-apply' && argv[0] !== 'compose-apply' &&
@@ -150,6 +154,10 @@ async function main(argv: readonly string[]): Promise<void> {
? commitLocalReconciliationPlanCommandFile(argv[2]!) ? commitLocalReconciliationPlanCommandFile(argv[2]!)
: argv[0] === 'reconciliation-plan-verify' : argv[0] === 'reconciliation-plan-verify'
? verifyLocalReconciliationPlanCommandFile(argv[2]!) ? verifyLocalReconciliationPlanCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-prepare'
? prepareLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-diagnostics'
? writeLocalReconciliationReviewDiagnosticsCommandFile(argv[2]!)
: argv[0] === 'compose-revision' : argv[0] === 'compose-revision'
? switchLocalDeploymentComposeRevisionCommandFile(argv[2]!) ? switchLocalDeploymentComposeRevisionCommandFile(argv[2]!)
: argv[0] === 'compose-preflight' : argv[0] === 'compose-preflight'
@@ -136,6 +136,13 @@ function classify(
return kind === 'legacy' ? legacyDomain(name) : targetDomain(name); return kind === 'legacy' ? legacyDomain(name) : targetDomain(name);
} }
export function classifyLocalReconciliationFact(
kind: LocalReconciliationSealedDatabaseKind,
name: string,
): LocalReconciliationPlanDomain {
return classify(kind, name);
}
function quotedIdentifier(name: string): string { function quotedIdentifier(name: string): string {
return `"${name.replaceAll('"', '""')}"`; return `"${name.replaceAll('"', '""')}"`;
} }
@@ -448,6 +448,35 @@ function readTerminal(
return Object.freeze({ plan, receipt }); return Object.freeze({ plan, receipt });
} }
export interface LocalReconciliationPlanTerminal {
readonly intent: Readonly<LocalReconciliationPlanIntent>;
readonly bundle: Readonly<LocalReconciliationSealedBundle>;
readonly plan: Readonly<LocalReconciliationPlan>;
readonly receipt: Readonly<LocalReconciliationPlanReceipt>;
}
export function readLocalReconciliationPlanTerminal(
planRoot: string,
planId: string,
uid: number,
): Readonly<LocalReconciliationPlanTerminal> {
const paths = planPaths(planRoot, planId);
const intent = readLocalReconciliationPlanIntent(planRoot, planId);
if (
intent.command.options.planRoot !== planRoot ||
intent.command.request.planId !== planId
) {
configurationError('terminal reconciliation plan path binding drifted');
}
const bundle = inspectLocalReconciliationSealedBundle(
intent.command.options.captureRoot,
intent.command.request.captureId,
uid,
);
const terminal = readTerminal(paths, intent, bundle, uid);
return Object.freeze({ intent, bundle, ...terminal });
}
function result( function result(
operation: LocalReconciliationPlanTerminalResult['operation'], operation: LocalReconciliationPlanTerminalResult['operation'],
status: LocalReconciliationPlanTerminalResult['status'], status: LocalReconciliationPlanTerminalResult['status'],
@@ -0,0 +1,315 @@
import path from 'node:path';
import { currentIdentity } from '../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import {
LOCAL_RECONCILIATION_PLAN_DOMAINS,
type LocalReconciliationPlanDomain,
} from '../planning/contract';
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 const LOCAL_RECONCILIATION_DIAGNOSTIC_FACT_KINDS = Object.freeze([
'schema_object',
'table',
] as const);
export type LocalReconciliationDiagnosticFactKind =
(typeof LOCAL_RECONCILIATION_DIAGNOSTIC_FACT_KINDS)[number];
export interface LocalReconciliationReviewOptions {
readonly deploymentRoot: string;
readonly captureRoot: string;
readonly planRoot: string;
readonly reviewRoot: string;
readonly allowRootService: boolean;
}
export interface LocalReconciliationReviewPrepareCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.prepare';
readonly options: Readonly<LocalReconciliationReviewOptions>;
readonly request: Readonly<{
reviewId: string;
planId: string;
expectedPlanDigest: string;
expectedHeadDigest: string;
preparedAtMs: number;
}>;
}
export interface LocalReconciliationReviewDiagnosticsCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.diagnostics';
readonly options: Readonly<LocalReconciliationReviewOptions>;
readonly request: Readonly<{
reviewId: string;
expectedPreparationDigest: string;
database: 'legacy' | 'target';
domain: LocalReconciliationPlanDomain;
factKind: LocalReconciliationDiagnosticFactKind;
offset: number;
limit: number;
outputPath: string;
}>;
}
export interface LocalReconciliationReviewPrepareResult {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.prepare';
readonly status: 'prepared' | 'existing';
readonly state: 'reconciliation_review_prepared';
readonly reviewId: string;
readonly preparationDigest: string;
readonly instanceHeadDigest: string;
}
export interface LocalReconciliationReviewDiagnosticsResult {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.diagnostics';
readonly status: 'prepared' | 'existing';
readonly state: 'reconciliation_review_prepared';
readonly reviewId: string;
readonly pageDigest: string;
readonly recordCount: number;
readonly complete: boolean;
readonly nextOffset: number | null;
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 safeAbsolutePath(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.normalize(value) !== value ||
path.parse(value).root === 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 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<LocalReconciliationReviewOptions> {
const options = object(value, 'options');
exact(
options,
[
'allowRootService',
'captureRoot',
'deploymentRoot',
'planRoot',
'reviewRoot',
],
'options',
);
const identity = currentIdentity();
if (
typeof options.allowRootService !== 'boolean' ||
(identity.uid === 0) !== options.allowRootService
) {
configurationError('reconciliation review command identity is invalid');
}
const roots = [
safeAbsolutePath(options.deploymentRoot, 'deploymentRoot'),
safeAbsolutePath(options.captureRoot, 'captureRoot'),
safeAbsolutePath(options.planRoot, 'planRoot'),
safeAbsolutePath(options.reviewRoot, 'reviewRoot'),
];
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 and review roots must not overlap');
}
}
}
return Object.freeze({
deploymentRoot: roots[0]!,
captureRoot: roots[1]!,
planRoot: roots[2]!,
reviewRoot: roots[3]!,
allowRootService: options.allowRootService,
});
}
function normalizeCommand(
value: unknown,
operation:
| 'local.deployment.reconciliation.review.prepare'
| 'local.deployment.reconciliation.review.diagnostics',
): Readonly<{
options: Readonly<LocalReconciliationReviewOptions>;
request: Record<string, unknown>;
}> {
const command = object(value, 'reconciliation review command');
exact(command, ['operation', 'options', 'request', 'schemaVersion'], 'command');
if (command.schemaVersion !== 1 || command.operation !== operation) {
configurationError('reconciliation review command is invalid');
}
return Object.freeze({
options: normalizeOptions(command.options),
request: object(command.request, 'request'),
});
}
export function normalizeLocalReconciliationReviewPrepareCommand(
value: unknown,
): Readonly<LocalReconciliationReviewPrepareCommand> {
const command = normalizeCommand(
value,
'local.deployment.reconciliation.review.prepare',
);
exact(
command.request,
[
'expectedHeadDigest',
'expectedPlanDigest',
'planId',
'preparedAtMs',
'reviewId',
],
'request',
);
if (
!Number.isSafeInteger(command.request.preparedAtMs) ||
(command.request.preparedAtMs as number) < 0
) {
configurationError('reconciliation review prepare command is invalid');
}
return Object.freeze({
schemaVersion: 1 as const,
operation: 'local.deployment.reconciliation.review.prepare' as const,
options: command.options,
request: Object.freeze({
reviewId: identifier(command.request.reviewId, 'reviewId'),
planId: identifier(command.request.planId, 'planId'),
expectedPlanDigest: digest(
command.request.expectedPlanDigest,
'expectedPlanDigest',
),
expectedHeadDigest: digest(
command.request.expectedHeadDigest,
'expectedHeadDigest',
),
preparedAtMs: command.request.preparedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationReviewDiagnosticsCommand(
value: unknown,
): Readonly<LocalReconciliationReviewDiagnosticsCommand> {
const command = normalizeCommand(
value,
'local.deployment.reconciliation.review.diagnostics',
);
exact(
command.request,
[
'database',
'domain',
'expectedPreparationDigest',
'factKind',
'limit',
'offset',
'outputPath',
'reviewId',
],
'request',
);
if (
(command.request.database !== 'legacy' &&
command.request.database !== 'target') ||
!LOCAL_RECONCILIATION_PLAN_DOMAINS.includes(
command.request.domain as LocalReconciliationPlanDomain,
) ||
!LOCAL_RECONCILIATION_DIAGNOSTIC_FACT_KINDS.includes(
command.request.factKind as LocalReconciliationDiagnosticFactKind,
) ||
!Number.isSafeInteger(command.request.offset) ||
(command.request.offset as number) < 0 ||
(command.request.offset as number) > 4_096 ||
!Number.isSafeInteger(command.request.limit) ||
(command.request.limit as number) < 1 ||
(command.request.limit as number) > 64
) {
configurationError('reconciliation review diagnostics command is invalid');
}
return Object.freeze({
schemaVersion: 1 as const,
operation: 'local.deployment.reconciliation.review.diagnostics' as const,
options: command.options,
request: Object.freeze({
reviewId: identifier(command.request.reviewId, 'reviewId'),
expectedPreparationDigest: digest(
command.request.expectedPreparationDigest,
'expectedPreparationDigest',
),
database: command.request.database,
domain: command.request.domain as LocalReconciliationPlanDomain,
factKind:
command.request.factKind as LocalReconciliationDiagnosticFactKind,
offset: command.request.offset as number,
limit: command.request.limit as number,
outputPath: safeAbsolutePath(command.request.outputPath, 'outputPath'),
}),
});
}
@@ -0,0 +1,494 @@
import fs from 'node:fs';
import path from 'node:path';
import type { DatabaseSync } from 'node:sqlite';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import { validatePrivateDirectory } from '../../foundation/files';
import { cutoverDigest } from '../../cutover/targetEvidence';
import { classifyLocalReconciliationFact } from '../planning/inventory';
import type { LocalReconciliationPlanDomain } from '../planning/contract';
import type {
LocalReconciliationSealedBundle,
LocalReconciliationSealedDatabaseKind,
} from '../sealed-bundle/reader';
import type {
LocalReconciliationDiagnosticFactKind,
LocalReconciliationReviewDiagnosticsCommand,
} from './contract';
const PAGE_SCHEMA = 'qinglong3-local-reconciliation-diagnostic-page';
const FACT_SCHEMA = 'qinglong3-local-reconciliation-diagnostic-fact';
const MAX_PAGE_BYTES = 256 * 1024;
const MAX_NAME_BYTES = 1_024;
export type LocalReconciliationDiagnosticDecisionRequirement =
| 'informational'
| 'required'
| 'blocked';
export type LocalReconciliationDiagnosticReason =
| 'catalog_evidence'
| 'reviewable_fact'
| 'unknown_schema'
| 'secret_custody_required'
| 'historical_integrity_required'
| 'identity_custody_required';
export interface LocalReconciliationDiagnosticFact {
readonly schema: typeof FACT_SCHEMA;
readonly schemaVersion: 1;
readonly ordinal: number;
readonly database: LocalReconciliationSealedDatabaseKind;
readonly domain: LocalReconciliationPlanDomain;
readonly factKind: LocalReconciliationDiagnosticFactKind;
readonly objectType: string;
readonly name: string;
readonly tableName: string;
readonly rowCount: string | null;
readonly decisionRequirement: LocalReconciliationDiagnosticDecisionRequirement;
readonly reason: LocalReconciliationDiagnosticReason;
readonly factDigest: string;
}
export interface LocalReconciliationDiagnosticPage {
readonly schema: typeof PAGE_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_review_prepared';
readonly reviewId: string;
readonly planId: string;
readonly planDigest: string;
readonly preparationDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly database: LocalReconciliationSealedDatabaseKind;
readonly domain: LocalReconciliationPlanDomain;
readonly factKind: LocalReconciliationDiagnosticFactKind;
readonly offset: number;
readonly limit: number;
readonly recordCount: number;
readonly complete: boolean;
readonly nextOffset: number | null;
readonly records: readonly Readonly<LocalReconciliationDiagnosticFact>[];
readonly pageDigest: string;
}
export interface BuildLocalReconciliationDiagnosticPageOptions {
readonly reviewId: string;
readonly planId: string;
readonly planDigest: string;
readonly preparationDigest: string;
readonly bundle: Readonly<LocalReconciliationSealedBundle>;
readonly command: Readonly<LocalReconciliationReviewDiagnosticsCommand>;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(message, { cause });
}
function quotedIdentifier(name: string): string {
return `"${name.replaceAll('"', '""')}"`;
}
function requirement(
domain: LocalReconciliationPlanDomain,
): Readonly<{
decisionRequirement: LocalReconciliationDiagnosticDecisionRequirement;
reason: LocalReconciliationDiagnosticReason;
}> {
if (domain === 'unknown') {
return Object.freeze({
decisionRequirement: 'blocked' as const,
reason: 'unknown_schema' as const,
});
}
if (domain === 'secret_and_config') {
return Object.freeze({
decisionRequirement: 'blocked' as const,
reason: 'secret_custody_required' as const,
});
}
if (domain === 'run_history') {
return Object.freeze({
decisionRequirement: 'blocked' as const,
reason: 'historical_integrity_required' as const,
});
}
if (domain === 'identity_policy_audit') {
return Object.freeze({
decisionRequirement: 'blocked' as const,
reason: 'identity_custody_required' as const,
});
}
if (domain === 'schema_lineage') {
return Object.freeze({
decisionRequirement: 'informational' as const,
reason: 'catalog_evidence' as const,
});
}
return Object.freeze({
decisionRequirement: 'required' as const,
reason: 'reviewable_fact' as const,
});
}
function rowCount(
client: DatabaseSync,
domain: LocalReconciliationPlanDomain,
tableName: string,
objectType: string,
): string | null {
if (domain === 'unknown' || objectType !== 'table') return null;
const counted = client
.prepare(
`SELECT CAST(COUNT(*) AS TEXT) AS countText FROM ${quotedIdentifier(
tableName,
)}`,
)
.get() as { readonly countText?: unknown } | undefined;
if (
typeof counted?.countText !== 'string' ||
!/^(?:0|[1-9][0-9]*)$/.test(counted.countText)
) {
configurationError('diagnostic table row count drifted');
}
return counted.countText;
}
function fact(
ordinal: number,
database: LocalReconciliationSealedDatabaseKind,
domain: LocalReconciliationPlanDomain,
factKind: LocalReconciliationDiagnosticFactKind,
objectType: string,
name: string,
tableName: string,
count: string | null,
): Readonly<LocalReconciliationDiagnosticFact> {
const review = requirement(domain);
const payload = Object.freeze({
schema: FACT_SCHEMA,
schemaVersion: 1 as const,
ordinal,
database,
domain,
factKind,
objectType,
name,
tableName,
rowCount: count,
decisionRequirement: review.decisionRequirement,
reason: review.reason,
});
return Object.freeze({ ...payload, factDigest: cutoverDigest(payload) });
}
function schemaFacts(
client: DatabaseSync,
database: LocalReconciliationSealedDatabaseKind,
domain: LocalReconciliationPlanDomain,
offset: number,
limit: number,
): Readonly<{ records: readonly Readonly<LocalReconciliationDiagnosticFact>[]; complete: boolean }> {
const rows = client
.prepare(
`SELECT type, name, tbl_name AS tableName
FROM sqlite_schema
WHERE name NOT LIKE 'sqlite_%'
ORDER BY type, name
LIMIT 4097`,
)
.iterate() as IterableIterator<{
readonly type?: unknown;
readonly name?: unknown;
readonly tableName?: unknown;
}>;
const records: LocalReconciliationDiagnosticFact[] = [];
let seen = 0;
let catalog = 0;
let complete = true;
for (const row of rows) {
catalog += 1;
if (catalog > 4_096) configurationError('diagnostic schema budget is exceeded');
if (
typeof row.type !== 'string' ||
typeof row.name !== 'string' ||
typeof row.tableName !== 'string' ||
Buffer.byteLength(row.name, 'utf8') > MAX_NAME_BYTES ||
Buffer.byteLength(row.tableName, 'utf8') > MAX_NAME_BYTES
) {
configurationError('diagnostic schema catalog drifted');
}
if (classifyLocalReconciliationFact(database, row.tableName) !== domain) {
continue;
}
const ordinal = seen + 1;
seen += 1;
if (seen <= offset) continue;
if (records.length >= limit) {
complete = false;
break;
}
records.push(
fact(
ordinal,
database,
domain,
'schema_object',
row.type,
row.name,
row.tableName,
null,
),
);
}
return Object.freeze({ records: Object.freeze(records), complete });
}
function tableFacts(
client: DatabaseSync,
database: LocalReconciliationSealedDatabaseKind,
domain: LocalReconciliationPlanDomain,
offset: number,
limit: number,
): Readonly<{ records: readonly Readonly<LocalReconciliationDiagnosticFact>[]; complete: boolean }> {
const rows = client
.prepare(
`SELECT name, type
FROM pragma_table_list
WHERE schema = 'main' AND name NOT LIKE 'sqlite_%'
ORDER BY name
LIMIT 513`,
)
.iterate() as IterableIterator<{
readonly name?: unknown;
readonly type?: unknown;
}>;
const records: LocalReconciliationDiagnosticFact[] = [];
let seen = 0;
let catalog = 0;
let complete = true;
for (const row of rows) {
catalog += 1;
if (catalog > 512) configurationError('diagnostic table budget is exceeded');
if (
typeof row.name !== 'string' ||
typeof row.type !== 'string' ||
Buffer.byteLength(row.name, 'utf8') > MAX_NAME_BYTES
) {
configurationError('diagnostic table catalog drifted');
}
if (classifyLocalReconciliationFact(database, row.name) !== domain) continue;
const ordinal = seen + 1;
seen += 1;
if (seen <= offset) continue;
if (records.length >= limit) {
complete = false;
break;
}
records.push(
fact(
ordinal,
database,
domain,
'table',
row.type,
row.name,
row.name,
rowCount(client, domain, row.name, row.type),
),
);
}
return Object.freeze({ records: Object.freeze(records), complete });
}
export function buildLocalReconciliationDiagnosticPage(
client: DatabaseSync,
options: Readonly<BuildLocalReconciliationDiagnosticPageOptions>,
): Readonly<LocalReconciliationDiagnosticPage> {
const request = options.command.request;
const selected =
request.factKind === 'schema_object'
? schemaFacts(
client,
request.database,
request.domain,
request.offset,
request.limit,
)
: tableFacts(
client,
request.database,
request.domain,
request.offset,
request.limit,
);
const nextOffset = selected.complete
? null
: request.offset + selected.records.length;
const payload = Object.freeze({
schema: PAGE_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_review_prepared' as const,
reviewId: options.reviewId,
planId: options.planId,
planDigest: options.planDigest,
preparationDigest: options.preparationDigest,
bundleDigest: options.bundle.receipt.bundleDigest,
bundleFingerprintDigest: options.bundle.fingerprintDigest,
database: request.database,
domain: request.domain,
factKind: request.factKind,
offset: request.offset,
limit: request.limit,
recordCount: selected.records.length,
complete: selected.complete,
nextOffset,
records: selected.records,
});
const page = Object.freeze({ ...payload, pageDigest: cutoverDigest(payload) });
if (Buffer.byteLength(`${JSON.stringify(page, null, 2)}\n`, 'utf8') > MAX_PAGE_BYTES) {
configurationError('diagnostic page exceeds its 256 KiB budget');
}
return page;
}
function pageBytes(page: Readonly<LocalReconciliationDiagnosticPage>): Buffer {
const bytes = Buffer.from(`${JSON.stringify(page, null, 2)}\n`, 'utf8');
if (bytes.byteLength < 2 || bytes.byteLength > MAX_PAGE_BYTES) {
configurationError('diagnostic page has an invalid size');
}
return bytes;
}
function pageFile(
filePath: string,
bytes: Buffer,
uid: number,
allowedLinks: readonly number[],
label: string,
): fs.Stats {
let stat: fs.Stats;
try {
stat = fs.lstatSync(filePath);
} catch (error) {
return configurationError(`${label} is unavailable`, error);
}
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
(stat.mode & 0o777) !== 0o600 ||
!allowedLinks.includes(stat.nlink) ||
stat.size !== bytes.byteLength ||
!fs.readFileSync(filePath).equals(bytes)
) {
configurationError(`${label} identity or content drifted`);
}
return stat;
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function writeStage(
stagePath: string,
bytes: Buffer,
uid: number,
): void {
let descriptor: number | undefined;
let created = false;
try {
descriptor = fs.openSync(
stagePath,
fs.constants.O_WRONLY |
fs.constants.O_CREAT |
fs.constants.O_EXCL |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
created = true;
fs.fchmodSync(descriptor, 0o600);
const stat = fs.fstatSync(descriptor);
if (!stat.isFile() || stat.uid !== uid || stat.nlink !== 1) {
configurationError('diagnostic page stage identity drifted');
}
let offset = 0;
while (offset < bytes.byteLength) {
const written = fs.writeSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
);
if (written < 1) configurationError('diagnostic page write stalled');
offset += written;
}
fs.fsyncSync(descriptor);
} catch (error) {
if (created) {
try {
fs.unlinkSync(stagePath);
} catch {
// A failed cleanup leaves a deterministic fail-closed stage.
}
}
if (error instanceof LocalDeploymentConfigurationError) throw error;
configurationError('diagnostic page stage cannot be written', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
export function publishLocalReconciliationDiagnosticPage(
outputPath: string,
page: Readonly<LocalReconciliationDiagnosticPage>,
uid: number,
): 'prepared' | 'existing' {
const parent = path.dirname(outputPath);
validatePrivateDirectory(parent, uid, 'diagnosticOutputParent');
const bytes = pageBytes(page);
const stagePath = path.join(
parent,
`.${path.basename(outputPath)}.ql3-review-page-stage`,
);
const existed = fs.existsSync(outputPath);
if (existed) pageFile(outputPath, bytes, uid, [1, 2], 'diagnostic page');
if (fs.existsSync(stagePath)) {
pageFile(stagePath, bytes, uid, [1, 2], 'diagnostic page stage');
} else if (!existed) {
writeStage(stagePath, bytes, uid);
}
if (!fs.existsSync(outputPath)) {
try {
fs.linkSync(stagePath, outputPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
configurationError('diagnostic page cannot be published', error);
}
}
syncDirectory(parent);
}
const target = pageFile(outputPath, bytes, uid, [1, 2], 'diagnostic page');
if (fs.existsSync(stagePath)) {
const stage = pageFile(
stagePath,
bytes,
uid,
[1, 2],
'diagnostic page stage',
);
if (target.dev !== stage.dev || target.ino !== stage.ino) {
configurationError('diagnostic page stage identity drifted');
}
fs.unlinkSync(stagePath);
syncDirectory(parent);
}
pageFile(outputPath, bytes, uid, [1], 'diagnostic page');
return existed ? 'existing' : 'prepared';
}
@@ -0,0 +1,517 @@
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 {
ensurePrivateDirectory,
preflightPublishedFile,
publishExactFile,
validatePrivateDirectory,
} from '../../foundation/files';
import {
advanceLocalCutoverInstanceHead,
readLocalCutoverInstanceHead,
type LocalCutoverInstanceHead,
} from '../../cutover/instanceLineage';
import { cutoverDigest } from '../../cutover/targetEvidence';
import { readLocalReconciliationPlanTerminal } from '../planning/preparation';
import {
withLocalReconciliationSealedDatabase,
type LocalReconciliationSealedBundleReaderDependencies,
} from '../sealed-bundle/reader';
import {
normalizeLocalReconciliationReviewDiagnosticsCommand,
normalizeLocalReconciliationReviewPrepareCommand,
type LocalReconciliationReviewDiagnosticsCommand,
type LocalReconciliationReviewDiagnosticsResult,
type LocalReconciliationReviewPrepareCommand,
type LocalReconciliationReviewPrepareResult,
} from './contract';
import {
buildLocalReconciliationDiagnosticPage,
publishLocalReconciliationDiagnosticPage,
} from './diagnostics';
const INTENT_SCHEMA = 'qinglong3-local-reconciliation-review-intent';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MAX_REVIEWS = 64;
export interface LocalReconciliationReviewIntent {
readonly schema: typeof INTENT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_review_prepared';
readonly command: Readonly<LocalReconciliationReviewPrepareCommand>;
readonly profile: 'edge' | 'standalone';
readonly instanceId: string;
readonly cutoverId: string;
readonly generation: number;
readonly activationDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly planReceiptDigest: string;
readonly plannedHeadDigest: string;
readonly preparationDigest: string;
}
export interface LocalReconciliationReviewDependencies
extends LocalReconciliationSealedBundleReaderDependencies {
readonly afterHeadPrepared?: () => void;
readonly beforeDiagnosticPublish?: () => void;
readonly afterDiagnosticPublish?: () => void;
}
interface LocalReconciliationReviewPaths {
readonly root: string;
readonly staging: string;
readonly intent: 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 contents(value: unknown): string {
return `${JSON.stringify(value, null, 2)}\n`;
}
function overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}
export function localReconciliationReviewDirectory(
reviewRoot: string,
reviewId: string,
): string {
return path.join(reviewRoot, reviewId);
}
function reviewPaths(
reviewRoot: string,
reviewId: string,
): Readonly<LocalReconciliationReviewPaths> {
const root = localReconciliationReviewDirectory(reviewRoot, reviewId);
return Object.freeze({
root,
staging: path.join(root, 'staging'),
intent: path.join(root, 'intent.json'),
});
}
function ensureReviewDirectory(
reviewRoot: string,
reviewId: string,
uid: number,
): Readonly<LocalReconciliationReviewPaths> {
const paths = reviewPaths(reviewRoot, reviewId);
const entries = fs.readdirSync(reviewRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || entry.isSymbolicLink()) {
configurationError('reconciliation review catalog contains drift');
}
}
if (entries.length >= MAX_REVIEWS && !fs.existsSync(paths.root)) {
configurationError('reconciliation review retention limit is reached');
}
ensurePrivateDirectory(paths.root, uid, 'reconciliationReviewDirectory');
ensurePrivateDirectory(paths.staging, uid, 'reconciliationReviewStaging');
return paths;
}
function validateCatalog(paths: Readonly<LocalReconciliationReviewPaths>): void {
const allowed = new Set([
'intent.json',
'staging',
'.intent.json.ql3-deploy-stage',
]);
for (const entry of fs.readdirSync(paths.root, { withFileTypes: true })) {
if (!allowed.has(entry.name) || entry.isSymbolicLink()) {
configurationError('reconciliation review root contains unknown material');
}
}
if (fs.readdirSync(paths.staging).length !== 0) {
configurationError('reconciliation review staging contains unknown material');
}
}
function buildIntent(
command: Readonly<LocalReconciliationReviewPrepareCommand>,
terminal: ReturnType<typeof readLocalReconciliationPlanTerminal>,
): Readonly<LocalReconciliationReviewIntent> {
if (
terminal.intent.command.options.deploymentRoot !==
command.options.deploymentRoot ||
terminal.intent.command.options.captureRoot !== command.options.captureRoot ||
terminal.intent.command.options.planRoot !== command.options.planRoot ||
terminal.intent.command.options.allowRootService !==
command.options.allowRootService ||
terminal.plan.planId !== command.request.planId ||
terminal.plan.planDigest !== command.request.expectedPlanDigest ||
command.request.preparedAtMs < terminal.plan.committedAtMs
) {
configurationError('reconciliation review is detached from its plan');
}
const payload = Object.freeze({
schema: INTENT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_review_prepared' as const,
command,
profile: terminal.plan.profile,
instanceId: terminal.intent.instanceId,
cutoverId: terminal.intent.cutoverId,
generation: terminal.intent.generation,
activationDigest: terminal.intent.activationDigest,
bundleDigest: terminal.plan.bundleDigest,
bundleFingerprintDigest: terminal.bundle.fingerprintDigest,
planReceiptDigest: terminal.receipt.receiptDigest,
plannedHeadDigest: command.request.expectedHeadDigest,
});
return Object.freeze({ ...payload, preparationDigest: cutoverDigest(payload) });
}
export function normalizeLocalReconciliationReviewIntent(
value: unknown,
): Readonly<LocalReconciliationReviewIntent> {
const intent = object(value, 'reconciliation review intent');
exact(
intent,
[
'activationDigest',
'bundleDigest',
'bundleFingerprintDigest',
'command',
'cutoverId',
'generation',
'instanceId',
'planReceiptDigest',
'plannedHeadDigest',
'preparationDigest',
'profile',
'schema',
'schemaVersion',
'state',
],
'reconciliation review intent',
);
const command = normalizeLocalReconciliationReviewPrepareCommand(intent.command);
const { preparationDigest, ...payload } = intent;
if (
intent.schema !== INTENT_SCHEMA ||
intent.schemaVersion !== 1 ||
intent.state !== 'reconciliation_review_prepared' ||
(intent.profile !== 'edge' && intent.profile !== 'standalone') ||
typeof intent.instanceId !== 'string' ||
intent.instanceId.length < 1 ||
intent.instanceId.length > 128 ||
typeof intent.cutoverId !== 'string' ||
intent.cutoverId.length < 1 ||
intent.cutoverId.length > 128 ||
!Number.isSafeInteger(intent.generation) ||
(intent.generation as number) < 1 ||
[
intent.activationDigest,
intent.bundleDigest,
intent.bundleFingerprintDigest,
intent.planReceiptDigest,
intent.plannedHeadDigest,
preparationDigest,
].some(
(candidate) =>
typeof candidate !== 'string' || !DIGEST_PATTERN.test(candidate),
) ||
cutoverDigest(payload) !== preparationDigest
) {
configurationError('reconciliation review intent drifted');
}
return Object.freeze({
...(intent as unknown as LocalReconciliationReviewIntent),
command,
});
}
export function readLocalReconciliationReviewIntent(
reviewRoot: string,
reviewId: string,
): Readonly<LocalReconciliationReviewIntent> {
return normalizeLocalReconciliationReviewIntent(
readPrivateLocalCommandFile(reviewPaths(reviewRoot, reviewId).intent),
);
}
function validateHeadIdentity(
head: Readonly<LocalCutoverInstanceHead>,
intent: Readonly<LocalReconciliationReviewIntent>,
): void {
if (
head.profile !== intent.profile ||
head.cutoverId !== intent.cutoverId ||
head.activationDigest !== intent.activationDigest ||
head.generation !== intent.generation
) {
configurationError('reconciliation review instance head identity drifted');
}
}
function advancePreparedHead(
intent: Readonly<LocalReconciliationReviewIntent>,
uid: number,
): Readonly<LocalCutoverInstanceHead> {
return advanceLocalCutoverInstanceHead(
{
options: { deploymentRoot: intent.command.options.deploymentRoot },
request: {
cutoverId: intent.cutoverId,
profile: intent.profile,
instanceId: intent.instanceId,
expectedActivationDigest: intent.activationDigest,
requestedAtMs: intent.command.request.preparedAtMs,
},
},
uid,
'reconciliation_review_prepared',
intent.generation,
intent.preparationDigest,
);
}
export function prepareLocalReconciliationReview(
input: unknown,
dependencies: LocalReconciliationReviewDependencies = {},
): Readonly<LocalReconciliationReviewPrepareResult> {
const command = normalizeLocalReconciliationReviewPrepareCommand(input);
const identity = currentIdentity();
validatePrivateDirectory(command.options.deploymentRoot, identity.uid, 'deploymentRoot');
validatePrivateDirectory(command.options.captureRoot, identity.uid, 'captureRoot');
validatePrivateDirectory(command.options.planRoot, identity.uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, identity.uid, 'reviewRoot');
const terminal = readLocalReconciliationPlanTerminal(
command.options.planRoot,
command.request.planId,
identity.uid,
);
const intent = buildIntent(command, terminal);
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
identity.uid,
);
validateHeadIdentity(head, intent);
if (
(head.state === 'reconciliation_planned' &&
(head.headDigest !== command.request.expectedHeadDigest ||
head.sourceRecordDigest !== command.request.expectedPlanDigest)) ||
(head.state === 'reconciliation_review_prepared' &&
head.sourceRecordDigest !== intent.preparationDigest) ||
(head.state !== 'reconciliation_planned' &&
head.state !== 'reconciliation_review_prepared')
) {
configurationError('review prepare lost the planned instance head compare-and-swap');
}
const paths = ensureReviewDirectory(
command.options.reviewRoot,
command.request.reviewId,
identity.uid,
);
const serialized = contents(intent);
preflightPublishedFile(
paths.intent,
serialized,
0o600,
identity.uid,
'reconciliation review intent',
);
const nextHead =
head.state === 'reconciliation_review_prepared'
? head
: advancePreparedHead(intent, identity.uid);
dependencies.afterHeadPrepared?.();
const status = publishExactFile(
paths.intent,
serialized,
0o600,
identity.uid,
'reconciliation review intent',
);
validateCatalog(paths);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
status,
state: 'reconciliation_review_prepared' as const,
reviewId: command.request.reviewId,
preparationDigest: intent.preparationDigest,
instanceHeadDigest: nextHead.headDigest,
});
}
function validateDiagnosticsBinding(
command: Readonly<LocalReconciliationReviewDiagnosticsCommand>,
intent: Readonly<LocalReconciliationReviewIntent>,
): void {
if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.captureRoot !== command.options.captureRoot ||
intent.command.options.planRoot !== command.options.planRoot ||
intent.command.options.reviewRoot !== command.options.reviewRoot ||
intent.command.options.allowRootService !== command.options.allowRootService ||
intent.command.request.reviewId !== command.request.reviewId ||
intent.preparationDigest !== command.request.expectedPreparationDigest
) {
configurationError('review diagnostics are detached from preparation');
}
const roots = [
command.options.deploymentRoot,
command.options.captureRoot,
command.options.planRoot,
command.options.reviewRoot,
];
if (
roots.some(
(root) =>
overlaps(root, command.request.outputPath) ||
overlaps(command.request.outputPath, root),
)
) {
configurationError('diagnostic output must be outside authority roots');
}
}
export function writeLocalReconciliationReviewDiagnostics(
input: unknown,
dependencies: LocalReconciliationReviewDependencies = {},
): Readonly<LocalReconciliationReviewDiagnosticsResult> {
const command = normalizeLocalReconciliationReviewDiagnosticsCommand(input);
const identity = currentIdentity();
validatePrivateDirectory(command.options.deploymentRoot, identity.uid, 'deploymentRoot');
validatePrivateDirectory(command.options.captureRoot, identity.uid, 'captureRoot');
validatePrivateDirectory(command.options.planRoot, identity.uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, identity.uid, 'reviewRoot');
const paths = reviewPaths(command.options.reviewRoot, command.request.reviewId);
validatePrivateDirectory(paths.root, identity.uid, 'reconciliationReviewDirectory');
validatePrivateDirectory(paths.staging, identity.uid, 'reconciliationReviewStaging');
const intent = readLocalReconciliationReviewIntent(
command.options.reviewRoot,
command.request.reviewId,
);
validateDiagnosticsBinding(command, intent);
const terminal = readLocalReconciliationPlanTerminal(
command.options.planRoot,
intent.command.request.planId,
identity.uid,
);
if (
terminal.plan.planDigest !== intent.command.request.expectedPlanDigest ||
terminal.plan.bundleDigest !== intent.bundleDigest ||
terminal.receipt.receiptDigest !== intent.planReceiptDigest ||
terminal.bundle.fingerprintDigest !== intent.bundleFingerprintDigest
) {
configurationError('review diagnostics lost the terminal plan binding');
}
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
identity.uid,
);
validateHeadIdentity(head, intent);
if (
head.state !== 'reconciliation_review_prepared' ||
head.sourceRecordDigest !== intent.preparationDigest
) {
configurationError('review diagnostics lost the prepared instance head');
}
const page = withLocalReconciliationSealedDatabase(
terminal.bundle,
command.request.database,
identity.uid,
dependencies,
(client) =>
buildLocalReconciliationDiagnosticPage(client, {
reviewId: intent.command.request.reviewId,
planId: intent.command.request.planId,
planDigest: terminal.plan.planDigest,
preparationDigest: intent.preparationDigest,
bundle: terminal.bundle,
command,
}),
);
if (page === null) {
configurationError('manual-required SQLite topology has no diagnostic page');
}
const currentHead = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
identity.uid,
);
if (
currentHead.headDigest !== head.headDigest ||
currentHead.state !== 'reconciliation_review_prepared' ||
currentHead.sourceRecordDigest !== intent.preparationDigest
) {
configurationError('review diagnostics instance head changed while reading');
}
dependencies.beforeDiagnosticPublish?.();
const status = publishLocalReconciliationDiagnosticPage(
command.request.outputPath,
page,
identity.uid,
);
dependencies.afterDiagnosticPublish?.();
validateCatalog(paths);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
status,
state: 'reconciliation_review_prepared' as const,
reviewId: command.request.reviewId,
pageDigest: page.pageDigest,
recordCount: page.recordCount,
complete: page.complete,
nextOffset: page.nextOffset,
instanceHeadDigest: currentHead.headDigest,
});
}
export function prepareLocalReconciliationReviewCommandFile(
filePath: string,
): Readonly<LocalReconciliationReviewPrepareResult> {
return prepareLocalReconciliationReview(readPrivateLocalCommandFile(filePath));
}
export function writeLocalReconciliationReviewDiagnosticsCommandFile(
filePath: string,
): Readonly<LocalReconciliationReviewDiagnosticsResult> {
return writeLocalReconciliationReviewDiagnostics(
readPrivateLocalCommandFile(filePath),
);
}
@@ -751,7 +751,9 @@ function replayResult(
(head.state === 'reconciliation_capture_prepared' || (head.state === 'reconciliation_capture_prepared' ||
head.state === 'reconciliation_captured' || head.state === 'reconciliation_captured' ||
head.state === 'reconciliation_plan_prepared' || head.state === 'reconciliation_plan_prepared' ||
head.state === 'reconciliation_planned'); head.state === 'reconciliation_planned' ||
head.state === 'reconciliation_review_prepared' ||
head.state === 'reconciliation_reviewed');
if ( if (
record.actionId !== intent.actionId || record.actionId !== intent.actionId ||
record.intentDigest !== intent.intentDigest || record.intentDigest !== intent.intentDigest ||
@@ -12,8 +12,10 @@ const {
commitLocalReconciliationPlan, commitLocalReconciliationPlan,
prepareLocalReconciliationCapture, prepareLocalReconciliationCapture,
prepareLocalReconciliationPlan, prepareLocalReconciliationPlan,
prepareLocalReconciliationReview,
verifyLocalReconciliationCapture, verifyLocalReconciliationCapture,
verifyLocalReconciliationPlan, verifyLocalReconciliationPlan,
writeLocalReconciliationReviewDiagnostics,
} = require('../dist/deployment/localDeployment.js'); } = require('../dist/deployment/localDeployment.js');
const { const {
normalizeLocalReconciliationCaptureManifest, normalizeLocalReconciliationCaptureManifest,
@@ -688,6 +690,74 @@ function preparedPlan(t, options = {}) {
}; };
} }
function preparedReview(t, options = {}) {
const state = preparedPlan(t, options);
const planned = commitLocalReconciliationPlan(state.planCommitCommand);
const root = path.dirname(state.captureRoot);
const reviewRoot = path.join(root, `review-root-${options.reviewSuffix ?? '1'}`);
const diagnosticRoot = path.join(
root,
`diagnostic-root-${options.reviewSuffix ?? '1'}`,
);
fs.mkdirSync(reviewRoot, { mode: 0o700 });
fs.mkdirSync(diagnosticRoot, { mode: 0o700 });
const reviewCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.review.prepare',
options: {
deploymentRoot: state.deploymentRoot,
captureRoot: state.captureRoot,
planRoot: state.planRoot,
reviewRoot,
allowRootService: rootAcknowledgement(),
},
request: {
reviewId:
options.reviewId ?? '00000000-0000-4000-8000-000000000301',
planId: state.prepareCommand.request.planId,
expectedPlanDigest: planned.planDigest,
expectedHeadDigest: planned.instanceHeadDigest,
preparedAtMs: 8_000,
},
};
return {
...state,
planned,
reviewRoot,
diagnosticRoot,
reviewCommand,
};
}
function diagnosticCommand(
state,
prepared,
{
database = 'legacy',
domain = 'automation',
factKind = 'table',
offset = 0,
limit = 64,
outputName = 'diagnostic-page.json',
} = {},
) {
return {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.review.diagnostics',
options: state.reviewCommand.options,
request: {
reviewId: state.reviewCommand.request.reviewId,
expectedPreparationDigest: prepared.preparationDigest,
database,
domain,
factKind,
offset,
limit,
outputPath: path.join(state.diagnosticRoot, outputName),
},
};
}
function dockerReadSealedSqlite(assetsDirectory, mode) { function dockerReadSealedSqlite(assetsDirectory, mode) {
const source = const source =
mode === 'main_only_immutable' mode === 'main_only_immutable'
@@ -1547,6 +1617,234 @@ test('plan prepare recovers head response loss and CLI verify stays content-free
); );
}); });
test('review prepare establishes one replayable fence and blocks rollback', (t) => {
const state = preparedReview(t, {
planId: '00000000-0000-4000-8000-000000000301',
reviewId: '00000000-0000-4000-8000-000000000311',
});
assert.throws(
() =>
prepareLocalReconciliationReview(state.reviewCommand, {
afterHeadPrepared() {
throw new Error('review prepare response loss');
},
}),
/review prepare response loss/,
);
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
assert.equal(prepared.status, 'prepared');
assert.equal(prepared.state, 'reconciliation_review_prepared');
const replay = prepareLocalReconciliationReview(state.reviewCommand);
assert.equal(replay.status, 'existing');
assert.equal(replay.preparationDigest, prepared.preparationDigest);
assert.equal(replay.instanceHeadDigest, prepared.instanceHeadDigest);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.command.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_review_prepared');
assert.equal(head.sourceRecordDigest, prepared.preparationDigest);
const competing = structuredClone(state.reviewCommand);
competing.request.reviewId = '00000000-0000-4000-8000-000000000312';
competing.options.reviewRoot = path.join(
path.dirname(state.reviewRoot),
'review-root-competing',
);
fs.mkdirSync(competing.options.reviewRoot, { mode: 0o700 });
assert.throws(
() => prepareLocalReconciliationReview(competing),
/compare-and-swap/,
);
assert.throws(
() =>
advanceLocalCutoverInstanceHead(
state.identity,
state.uid,
'rollback_prepared',
1,
'f'.repeat(64),
),
/transition is invalid/,
);
assert.throws(
() =>
advanceLocalCutoverInstanceHead(
state.identity,
state.uid,
'target_active',
2,
'e'.repeat(64),
),
/transition is invalid/,
);
});
test('review diagnostics publish one private exact page without changing assets', (t) => {
const state = preparedReview(t, {
planId: '00000000-0000-4000-8000-000000000321',
reviewId: '00000000-0000-4000-8000-000000000322',
});
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
const assets = fs.readdirSync(capturePath(state, 'assets')).map((name) => ({
name,
bytes: fs.readFileSync(capturePath(state, `assets/${name}`)),
stat: fs.statSync(capturePath(state, `assets/${name}`), { bigint: true }),
}));
const command = diagnosticCommand(state, prepared);
const opens = [];
const result = writeLocalReconciliationReviewDiagnostics(command, {
beforeDatabaseOpen(kind, mode, cacheKiB) {
opens.push({ kind, mode, cacheKiB });
},
});
assert.deepEqual(opens, [
{ kind: 'legacy', mode: 'main_only_immutable', cacheKiB: 2048 },
]);
assert.equal(result.status, 'prepared');
assert.equal(result.recordCount, 1);
assert.equal(result.complete, true);
assert.equal(result.nextOffset, null);
assert.equal(JSON.stringify(result).includes(command.request.outputPath), false);
assert.equal(JSON.stringify(result).includes('Crontabs'), false);
const pageText = fs.readFileSync(command.request.outputPath, 'utf8');
const page = JSON.parse(pageText);
assert.equal(page.records[0].name, 'Crontabs');
assert.equal(page.records[0].rowCount, '1');
assert.equal(page.records[0].decisionRequirement, 'required');
assert.equal(pageText.includes('0 0 * * *'), false);
assert.equal(pageText.includes('private-value'), false);
assert.equal(fs.statSync(command.request.outputPath).mode & 0o777, 0o600);
assert.equal(
writeLocalReconciliationReviewDiagnostics(command).status,
'existing',
);
for (const before of assets) {
const assetPath = capturePath(state, `assets/${before.name}`);
const after = fs.statSync(assetPath, { bigint: true });
assert.equal(fs.readFileSync(assetPath).equals(before.bytes), true);
assert.equal(after.mtimeNs, before.stat.mtimeNs);
assert.equal(after.ctimeNs, before.stat.ctimeNs);
assert.equal(after.mode, before.stat.mode);
}
});
test('review diagnostics keep secret and unknown facts blocked and row-free', (t) => {
const state = preparedReview(t, {
unknownTargetTable: true,
planId: '00000000-0000-4000-8000-000000000331',
reviewId: '00000000-0000-4000-8000-000000000332',
});
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
const secretCommand = diagnosticCommand(state, prepared, {
domain: 'secret_and_config',
outputName: 'secret.json',
});
writeLocalReconciliationReviewDiagnostics(secretCommand);
const secretText = fs.readFileSync(secretCommand.request.outputPath, 'utf8');
const secret = JSON.parse(secretText);
assert.equal(secret.records[0].name, 'Envs');
assert.equal(secret.records[0].decisionRequirement, 'blocked');
assert.equal(secret.records[0].reason, 'secret_custody_required');
assert.equal(secretText.includes('TOKEN'), false);
assert.equal(secretText.includes('private-value'), false);
const unknownCommand = diagnosticCommand(state, prepared, {
database: 'target',
domain: 'unknown',
outputName: 'unknown.json',
});
writeLocalReconciliationReviewDiagnostics(unknownCommand);
const unknown = JSON.parse(
fs.readFileSync(unknownCommand.request.outputPath, 'utf8'),
);
assert.equal(unknown.records[0].name, 'UnreviewedFacts');
assert.equal(unknown.records[0].rowCount, null);
assert.equal(unknown.records[0].decisionRequirement, 'blocked');
assert.equal(unknown.records[0].reason, 'unknown_schema');
const overlapping = structuredClone(unknownCommand);
overlapping.request.outputPath = path.join(state.reviewRoot, 'leak.json');
assert.throws(
() => writeLocalReconciliationReviewDiagnostics(overlapping),
/outside authority roots/,
);
});
test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => {
const initializeDatabases = (paths) => {
planningDatabaseInitializer()(paths);
const target = new DatabaseSync(paths.targetDatabasePath);
for (let index = 0; index < 70; index += 1) {
target.exec(
`CREATE TABLE "QingLong3TaskDefinitionExtra${String(index).padStart(
2,
'0',
)}" (id INTEGER PRIMARY KEY)`,
);
}
target.close();
fs.chmodSync(paths.targetDatabasePath, 0o600);
};
const state = preparedReview(t, {
initializeDatabases,
planId: '00000000-0000-4000-8000-000000000341',
reviewId: '00000000-0000-4000-8000-000000000342',
});
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
const firstCommand = diagnosticCommand(state, prepared, {
database: 'target',
domain: 'automation',
factKind: 'schema_object',
outputName: 'page-1.json',
});
const first = writeLocalReconciliationReviewDiagnostics(firstCommand);
assert.equal(first.recordCount, 64);
assert.equal(first.complete, false);
assert.equal(first.nextOffset, 64);
const secondCommand = diagnosticCommand(state, prepared, {
database: 'target',
domain: 'automation',
factKind: 'schema_object',
offset: first.nextOffset,
outputName: 'page-2.json',
});
const second = writeLocalReconciliationReviewDiagnostics(secondCommand);
assert.equal(second.recordCount, 7);
assert.equal(second.complete, true);
assert.equal(second.nextOffset, null);
const cliCommand = diagnosticCommand(state, prepared, {
database: 'target',
domain: 'automation',
factKind: 'table',
limit: 1,
outputName: 'cli-page.json',
});
const commandPath = path.join(state.deploymentRoot, 'review-diagnostic.json');
fs.writeFileSync(commandPath, `${JSON.stringify(cliCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-review-diagnostics',
'--command-file',
commandPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
const output = JSON.parse(cli.stdout);
assert.equal(output.recordCount, 1);
assert.equal(cli.stdout.includes(state.reviewRoot), false);
assert.equal(cli.stdout.includes(state.diagnosticRoot), false);
assert.equal(cli.stdout.includes('QingLong3TaskDefinitions'), false);
assert.equal(cli.stderr, '');
});
test( test(
'real stopped Docker target produces an independently verified bundle', 'real stopped Docker target produces an independently verified bundle',
{ skip: process.env.QL3_RECONCILIATION_DOCKER_GATE !== '1' }, { skip: process.env.QL3_RECONCILIATION_DOCKER_GATE !== '1' },
+2 -2
View File
@@ -207,10 +207,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localOwnerCli.rootSourceFileRoles, rootSourceFileRoles: localOwnerCli.rootSourceFileRoles,
}, },
{ {
sourceFiles: 146, sourceFiles: 149,
rootSourceFiles: 1, rootSourceFiles: 1,
rootSourceLines: 50, rootSourceLines: 50,
nestedSourceFiles: 145, nestedSourceFiles: 148,
rootSourceFileRoles: { 'cli.ts': 'binary_entry' }, rootSourceFileRoles: { 'cli.ts': 'binary_entry' },
}, },
); );