fix(ql3): preserve target-native reconciliation domains

This commit is contained in:
whyour
2026-08-24 13:55:54 +08:00
parent d94ca02e0d
commit fd10f72aba
10 changed files with 244 additions and 125 deletions
+12 -3
View File
@@ -58,10 +58,19 @@
`Secret/Config backup.sha256 == Automation targetAfter.sha256`,再证明 current target 等于 Secret/Config `targetAfter`。lineage 只新增 `Secret/Config backup.sha256 == Automation targetAfter.sha256`,再证明 current target 等于 Secret/Config `targetAfter`。lineage 只新增
`reconciliation_secret_config_applied → reconciliation_completed` 一条合法边;receipt seal 与 completed head durable 后,才幂等删除两类数据库等量 `reconciliation_secret_config_applied → reconciliation_completed` 一条合法边;receipt seal 与 completed head durable 后,才幂等删除两类数据库等量
backup。Secret/Config 的 ciphertext material、intent 与 receipt 继续 `0400` 保留,backup/rollback root 封为 `0500`,不增加后台 GC、timer 或常驻内存。 backup。Secret/Config 的 ciphertext material、intent 与 receipt 继续 `0400` 保留,backup/rollback root 封为 `0500`,不增加后台 GC、timer 或常驻内存。
rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭;完整迁移库当前仍可能因 Identity/Unknown 保持 manual,这不是 v3 可绕过的限制 rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭。
第九切片以 D-398/ADR-0493 修正 planner 对目标原生数据的两个架构误判,而没有新增 adapter 或放宽未知 schema。fresh v52 中
`QingLong3LegacyAdoptions``QingLong3LegacyAdoptionTasks``QingLong3LocalExecutionContextRecipes``QingLong3LocalTaskExecutionRevisions`
精确归入 Automation`QingLong3RunAttemptLogArtifactTombstones``QingLong3RunAttemptLogRetentionState` 精确归入 Run History;不使用
`QingLong3*` 宽泛通配,未来未登记表仍是 row-free、blocked 的 `unknown``identity_policy_audit` 按来源分离:没有 Legacy `Auths/Users`
时,Target 原生 Project/RoleBinding/Identity/Credential/Pepper/Approval/SecurityAudit/LocalOwner 事实为 `target_only`,必须逐事实 signed
`retain_target`,随后才以 `no_effect/application_summary` 进入 completion;只要存在 Legacy 身份事实,仍为
`manual_required/identity_custody_required`,本切片不声称旧 credential、session、token、Policy 或 Audit 已迁移。Secret/Config v52 fixture
现已用真实 `complete → replay → verify` 完成 v3 闭环,不再由测试伪造 receipt、直接推进 head 或手工回收 storage;真正未知表和 Legacy 身份回归
继续失败关闭。
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`publisher 定向回归 `6/6`fresh Edge 全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`publisher 定向回归 `6/6`fresh Edge
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`Local Owner 为 readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`ADR-0493 后 Local Owner 有效结果
`300 total / 293 pass / 7 conditional skip / 0 fail`18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为 `301 total / 294 pass / 7 conditional skip / 0 fail`;完整 backend 为
`1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、本地镜像与 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、本地镜像与
`14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB `14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB
上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。 上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。
@@ -143,6 +143,6 @@ Cluster 不得把 Legacy Env 明文写入 PostgreSQL、ConfigMap、Job command
D-397 当前八切片已经实现:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify、v51 逐 Task/Trigger adoption provenance、v52 Local SQLite 原子 application publisher、Owner prepared/apply/rollback orchestration,以及 ADR-0492 completion v3。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledgerdeferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。Owner 在写前固定 backup 与 stopped proof,以有界 ciphertext-only material 连接 reviewed decision 和 publisher,并覆盖 apply/rollback 全部 response-loss 窗口。completion v3 保留 v1/v2 exact shape,验证 signed decision/apply/current target/head,只有 completed head durable 后才幂等回收 Secret/Config backuprolled-back、target drift 与其余 manual 域继续失败关闭。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DMLrollback 则恢复写前 snapshot。 D-397 当前八切片已经实现:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify、v51 逐 Task/Trigger adoption provenance、v52 Local SQLite 原子 application publisher、Owner prepared/apply/rollback orchestration,以及 ADR-0492 completion v3。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledgerdeferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。Owner 在写前固定 backup 与 stopped proof,以有界 ciphertext-only material 连接 reviewed decision 和 publisher,并覆盖 apply/rollback 全部 response-loss 窗口。completion v3 保留 v1/v2 exact shape,验证 signed decision/apply/current target/head,只有 completed head durable 后才幂等回收 Secret/Config backuprolled-back、target drift 与其余 manual 域继续失败关闭。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DMLrollback 则恢复写前 snapshot。
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`Local Owner 为 `300 total / 293 pass / 7 conditional skip / 0 fail`18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatibleLocal Admin 为 49 source / 48 nested / 1 root exportLocal Owner 为 188/187/1workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。 本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`ADR-0493 后 Local Owner 有效结果`301 total / 294 pass / 7 conditional skip / 0 fail`完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatibleLocal Admin 为 49 source / 48 nested / 1 root exportLocal Owner 为 188/187/1workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gateADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,但本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管,也不得把仍含 Identity/Unknown manual 域的迁移冒充跨领域 completion 已完成 转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gateADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,ADR-0493 又让没有 Legacy 身份输入的 fresh v52 目标身份经 signed `retain_target` 正确形成 no-effect,并精确消除六张已知目标表的 `unknown` 误判。Legacy `Auths/Users` 或真正未知表仍保持 manual;本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管
@@ -56,7 +56,7 @@ Cluster 不复用本机 receipt、POSIX storage 或 instance head。Cluster comp
- completion receipt 的消费者必须按 `schemaVersion` 解析,未知版本继续失败关闭。 - completion receipt 的消费者必须按 `schemaVersion` 解析,未知版本继续失败关闭。
- `advanceLocalCutoverInstanceHead` 是 CRITICAL 共享状态机;本 ADR 只增加一条 source-state 边,不改任何旧 transition。 - `advanceLocalCutoverInstanceHead` 是 CRITICAL 共享状态机;本 ADR 只增加一条 source-state 边,不改任何旧 transition。
- 完整迁移库当前仍可能因 `identity_policy_audit` `unknown` 保持 manual这是正确的全局围栏,不应为了演示 v3 成功而放宽 - 本 ADR 验收时完整迁移库仍会因目标原生 `identity_policy_audit` 和六张未登记 v52 表落入 `unknown` 保持 manualADR-0493 已通过精确分类与 signed `retain_target` 修正这两个本地 planner 缺口。Legacy `Auths/Users` 和真正未知表仍保持全局围栏
- v3 verify 要求 completed storage,不能在 head durable 之前充当修复命令。 - v3 verify 要求 completed storage,不能在 head durable 之前充当修复命令。
## 被拒绝的替代方案 ## 被拒绝的替代方案
@@ -0,0 +1,73 @@
# ADR-0493:目标原生 Reconciliation 域分类与身份保留
- 状态:Accepted
- 日期:2026-08-24
- 决策:D-398
- 关联:ADR-0483、ADR-0484、ADR-0485、ADR-0488、ADR-0492
## 背景
Reconciliation 必须同时处理两类不同事实:从 2.x 迁入的数据,以及已经属于 QingLong 3.0 目标库的原生数据。此前实现把 `identity_policy_audit` 整域无条件标为 `manual_required/identity_custody_required`,因此即使 Legacy 完全没有 `Auths/Users`fresh v52 目标库自带的 Project、RoleBinding、Credential、Pepper、SecurityAudit 与 LocalOwner 表也会阻止全局 completion。
同一盘点还发现 fresh v52 的六张已知表没有进入稳定领域,被误归为 `unknown`
- Automation`QingLong3LegacyAdoptions``QingLong3LegacyAdoptionTasks``QingLong3LocalExecutionContextRecipes``QingLong3LocalTaskExecutionRevisions`
- Run History`QingLong3RunAttemptLogArtifactTombstones``QingLong3RunAttemptLogRetentionState`
这不是缺少新 adapter,而是现有 planner 对目标原生 schema 的知识不完整。为绕过门禁而伪造 completion receipt 或把整个 `QingLong3*` 前缀视为可信都会破坏 fail-closed 边界。
## 决策
### 1. 已知目标表使用精确领域目录
上述六张表按精确表名分别归入 Automation 与 Run History。Schema object 继续按其 `tableName` 继承同一领域,因此相关 index/trigger 不需要单独的宽泛名称规则。
不引入 `QingLong3*``LegacyAdoption*``Local*` 的整体通配。未来新增但尚未登记的目标表仍进入 `unknown`,不读取行数,并要求人工外部处理。
### 2. 目标原生身份是保留事实,不是迁移事实
`identity_policy_audit` 的 disposition 改为按来源判定:
- Legacy 存在 `Auths/Users` 时继续 `manual_required`
- Legacy 没有身份事实、Target 有已知身份事实时为 `target_only`
- 两端都没有时为 `aligned`
Target 身份诊断是 `required/reviewable_fact`,必须在 signed review 中逐事实选择 `retain_target` 才能形成 `no_effect` application action。它不能选择 `adopt_legacy``exclude_legacy`。Legacy 身份诊断仍是 `blocked/identity_custody_required`,只允许 `defer|manual_external`,本 ADR 不声明 2.x credential、session、password hash、token、Policy 或 Audit 已迁移。
### 3. Completion 只消费既有闭合证据
本 ADR 不增加 completion schema、adapter、instance state 或绕过规则。目标原生身份经过 canonical diagnostics、signed decision 和 application summary 后,以既有 `no_effect/application_summary` 进入 completion;真正未知表和任何 Legacy 身份事实仍阻止 completion。
完整 v52 + Legacy Secret/Config fixture 必须通过真实 v3 completion,而不是测试直接构造 receipt、推进 head 或调用 storage collector。只有 completed head durable 后,Secret/Config backup 才能由既有流程回收。
### 4. 部署与资源边界不变
实现只修改既有 Local Owner 一次性 reconciliation planner/reviewer,没有新增 workspace package、production dependency、SQL migration、daemon、listener、timer、Pool、容器或 Kubernetes workload。Edge/Standalone 常驻闭包、SQLite cache 上限和 artifact 预算不变;Cluster 仍使用自己的 PostgreSQL、Secret provider 与 HA authority。
## 被拒绝的替代方案
### 为目标原生身份增加 migration adapter
拒绝。没有 Legacy 身份输入时不存在要迁移的数据;额外 adapter 只会复制已经由目标数据库拥有的事实并扩大权限面。
### 信任全部 QingLong3 前缀
拒绝。名称前缀不是 schema ownership 证明,会让未来未知表自动越过人工审查。
### 自动跳过目标身份评审
拒绝。`target_only` 仍必须形成逐事实 signed `retain_target` 决策,不能仅凭分类器直接授权 completion。
### 放行 Legacy Auths/Users
拒绝。旧身份材料的 custody、hash/token 兼容、撤销、Policy 与 Audit 语义尚未建立独立 adapter;本 ADR 只消除目标原生数据被误当成迁移输入的问题。
## 验证
- 定向回归 `4/4`fresh v52 `unknown=0`、目标身份 `retain_target`、Legacy 身份继续 blocked、真正未知表继续 row-free/manual
- Secret/Config completion v3 使用真实 `complete → replay → verify` 链路,首调 `completed`、重放 `existing`receipt 为 v3 且 Identity/Unknown 均为 `no_effect`
- completed head durable 后 Secret/Config backup 被回收,加密 material 与 receipt 保持只读;
- Local Owner 受限沙箱为 `301 total / 291 pass / 7 conditional skip / 3 loopback EPERM`,两个 loopback 文件在沙箱外 `15/15`,有效结果 `301/294/7/0`
- 完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`18-package clean build 与逐包顺序测试除 Worker 三条 sandbox loopback 外全部通过,对应 Worker 文件在沙箱外 `8/8`
- package boundary、精确 Cluster dependency、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatibleworkspace 保持 18 packages`singleSourcePackages=[]``shallowSourcePackages=[]`
- 基础 Edge/Standalone 保持 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,没有 Cluster/PostgreSQL 闭包。
+1
View File
@@ -496,6 +496,7 @@
| [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | Accepted | | [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | Accepted |
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 local apply/rollback/completion completeEdge/Cluster gate pending | | [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 local apply/rollback/completion completeEdge/Cluster gate pending |
| [ADR-0492](./ADR-0492-compatible-secret-config-completion-v3.md) | 兼容 Secret/Config 的 Reconciliation Completion v3 | Accepted | | [ADR-0492](./ADR-0492-compatible-secret-config-completion-v3.md) | 兼容 Secret/Config 的 Reconciliation Completion v3 | Accepted |
| [ADR-0493](./ADR-0493-target-native-reconciliation-domain-preservation.md) | 目标原生 Reconciliation 域分类与身份保留 | Accepted |
## 规则 ## 规则
@@ -16,6 +16,16 @@ import {
const MAX_SCHEMA_OBJECTS = 4_096; const MAX_SCHEMA_OBJECTS = 4_096;
const MAX_TABLES = 512; const MAX_TABLES = 512;
const TARGET_AUTOMATION_TABLES = new Set([
'QingLong3LegacyAdoptions',
'QingLong3LegacyAdoptionTasks',
'QingLong3LocalExecutionContextRecipes',
'QingLong3LocalTaskExecutionRevisions',
]);
const TARGET_RUN_HISTORY_TABLES = new Set([
'QingLong3RunAttemptLogArtifactTombstones',
'QingLong3RunAttemptLogRetentionState',
]);
export interface LocalReconciliationDomainInventory { export interface LocalReconciliationDomainInventory {
readonly domain: LocalReconciliationPlanDomain; readonly domain: LocalReconciliationPlanDomain;
@@ -85,6 +95,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
return 'schema_lineage'; return 'schema_lineage';
} }
if ( if (
TARGET_AUTOMATION_TABLES.has(name) ||
name.includes('TaskDefinition') || name.includes('TaskDefinition') ||
name.includes('Trigger') || name.includes('Trigger') ||
name.includes('Automation') name.includes('Automation')
@@ -95,6 +106,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
return 'secret_and_config'; return 'secret_and_config';
} }
if ( if (
TARGET_RUN_HISTORY_TABLES.has(name) ||
name === 'Runs' || name === 'Runs' ||
name.startsWith('Run') || name.startsWith('Run') ||
name.startsWith('StepRun') || name.startsWith('StepRun') ||
@@ -175,10 +175,11 @@ function domainDisposition(
) { ) {
return 'unsupported'; return 'unsupported';
} }
if (domain === 'identity_policy_audit' && legacyFacts > 0) {
return 'manual_required';
}
if ( if (
(domain === 'secret_and_config' || (domain === 'secret_and_config' || domain === 'run_history') &&
domain === 'run_history' ||
domain === 'identity_policy_audit') &&
legacyFacts + targetFacts > 0 legacyFacts + targetFacts > 0
) { ) {
return 'manual_required'; return 'manual_required';
@@ -297,10 +297,14 @@ export function assertLocalReconciliationReviewDecisionMatchesFact(
!['retain_target', 'retain_both', 'defer', 'manual_external'].includes( !['retain_target', 'retain_both', 'defer', 'manual_external'].includes(
selected.disposition, selected.disposition,
)) || )) ||
((fact.domain === 'secret_and_config' || ((fact.domain === 'secret_and_config' || fact.domain === 'unknown') &&
fact.domain === 'identity_policy_audit' || !['defer', 'manual_external'].includes(selected.disposition)) ||
fact.domain === 'unknown') && (fact.domain === 'identity_policy_audit' &&
!['defer', 'manual_external'].includes(selected.disposition)) !(
fact.database === 'target'
? ['retain_target', 'defer', 'manual_external']
: ['defer', 'manual_external']
).includes(selected.disposition))
) { ) {
configurationError( configurationError(
'decision disposition is not allowed for canonical fact', 'decision disposition is not allowed for canonical fact',
@@ -195,6 +195,12 @@ function requirement(
}); });
} }
if (domain === 'identity_policy_audit') { if (domain === 'identity_policy_audit') {
if (database === 'target') {
return Object.freeze({
decisionRequirement: 'required' as const,
reason: 'reviewable_fact' as const,
});
}
return Object.freeze({ return Object.freeze({
decisionRequirement: 'blocked' as const, decisionRequirement: 'blocked' as const,
reason: 'identity_custody_required' as const, reason: 'identity_custody_required' as const,
@@ -74,17 +74,6 @@ const {
const { const {
targetStoppedEvidence, targetStoppedEvidence,
} = require('../dist/deployment/cutover/targetStopRecordEvidence.js'); } = require('../dist/deployment/cutover/targetStopRecordEvidence.js');
const {
collectLocalReconciliationSecretConfigCompletedStorage,
localReconciliationSecretConfigApplyPaths,
readLocalReconciliationSecretConfigApplyIntent,
validateLocalReconciliationSecretConfigCompletedStorage,
} = require('../dist/deployment/reconciliation/application/secret-and-config/application/storage.js');
const {
buildLocalReconciliationCompletionReceipt,
normalizeLocalReconciliationCompletionReceipt,
} = require('../dist/deployment/reconciliation/completion/evidence.js');
function digest(value) { function digest(value) {
return crypto return crypto
.createHash('sha256') .createHash('sha256')
@@ -2974,6 +2963,55 @@ test('review diagnostics keep secret and unknown facts blocked and row-free', (t
); );
}); });
test('review diagnostics preserve target-native identity but keep legacy identity fail-closed', (t) => {
const initializeDatabases = (paths) => {
planningDatabaseInitializer()(paths);
const legacy = new DatabaseSync(paths.legacySourcePath);
legacy.exec('CREATE TABLE "Auths" (id INTEGER PRIMARY KEY)');
legacy.close();
fs.copyFileSync(paths.legacySourcePath, paths.recoveryPath);
fs.chmodSync(paths.recoveryPath, 0o600);
const target = new DatabaseSync(paths.targetDatabasePath);
target.exec(
'CREATE TABLE "QingLong3IdentityRecords" (id INTEGER PRIMARY KEY)',
);
target.close();
};
const state = preparedReview(t, {
initializeDatabases,
planId: '00000000-0000-4000-8000-000000000333',
reviewId: '00000000-0000-4000-8000-000000000334',
reviewSuffix: 'identity-custody',
});
assert.equal(state.planned.outcome, 'manual_required');
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
const legacyCommand = diagnosticCommand(state, prepared, {
database: 'legacy',
domain: 'identity_policy_audit',
outputName: 'legacy-identity.json',
});
writeLocalReconciliationReviewDiagnostics(legacyCommand);
const legacy = JSON.parse(
fs.readFileSync(legacyCommand.request.outputPath, 'utf8'),
);
assert.equal(legacy.records[0].name, 'Auths');
assert.equal(legacy.records[0].decisionRequirement, 'blocked');
assert.equal(legacy.records[0].reason, 'identity_custody_required');
const targetCommand = diagnosticCommand(state, prepared, {
database: 'target',
domain: 'identity_policy_audit',
outputName: 'target-identity.json',
});
writeLocalReconciliationReviewDiagnostics(targetCommand);
const target = JSON.parse(
fs.readFileSync(targetCommand.request.outputPath, 'utf8'),
);
assert.equal(target.records[0].name, 'QingLong3IdentityRecords');
assert.equal(target.records[0].decisionRequirement, 'required');
assert.equal(target.records[0].reason, 'reviewable_fact');
});
test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => { test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => {
const initializeDatabases = (paths) => { const initializeDatabases = (paths) => {
planningDatabaseInitializer()(paths); planningDatabaseInitializer()(paths);
@@ -4352,7 +4390,7 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
assert.equal(databaseCloses, authentications); assert.equal(databaseCloses, authentications);
}); });
test('completion v3 proves Secret/Config apply but preserves rollback authority while other domains remain manual', async (t) => { test('completion v3 completes a classified v52 target and collects Secret/Config rollback authority', async (t) => {
const state = await appliedSecretConfigFixture(t, { const state = await appliedSecretConfigFixture(t, {
suffix: 'completion-v3', suffix: 'completion-v3',
}); });
@@ -4402,116 +4440,71 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
const backupPath = path.join(backupRoot, 'before.sqlite'); const backupPath = path.join(backupRoot, 'before.sqlite');
const materialPath = path.join(applyRoot, 'materials.ndjson'); const materialPath = path.join(applyRoot, 'materials.ndjson');
assert.equal(fs.existsSync(backupPath), true); assert.equal(fs.existsSync(backupPath), true);
const plan = JSON.parse(
await assert.rejects( fs.readFileSync(
completeLocalReconciliation(command), path.join(
/identity_policy_audit is not terminally reconciled/, state.planRoot,
state.prepareCommand.request.planId,
'plan.json',
),
'utf8',
),
); );
assert.equal(fs.existsSync(backupPath), true); assert.deepEqual(
plan.domains.find((domain) => domain.domain === 'unknown'),
{
domain: 'unknown',
legacySchemaObjects: 0,
targetSchemaObjects: 0,
legacyTables: 0,
targetTables: 0,
legacyRows: 0,
targetRows: 0,
rowCountsComplete: true,
inventoryDigest: plan.domains.find(
(domain) => domain.domain === 'unknown',
).inventoryDigest,
disposition: 'aligned',
},
);
assert.equal(
plan.domains.find((domain) => domain.domain === 'identity_policy_audit')
.disposition,
'target_only',
);
const targetIdentityDecisions = state.reviewFile.decisions.filter(
(decision) =>
decision.database === 'target' &&
decision.domain === 'identity_policy_audit',
);
assert.ok(targetIdentityDecisions.length > 0);
assert.equal(
targetIdentityDecisions.every(
(decision) => decision.disposition === 'retain_target',
),
true,
);
const completed = await completeLocalReconciliation(command);
assert.equal(completed.status, 'completed');
assert.equal(completed.state, 'reconciliation_completed');
assert.equal(completed.adapterCount, 1);
assert.equal((await completeLocalReconciliation(command)).status, 'existing');
assert.equal( assert.equal(
readLocalCutoverInstanceHead( readLocalCutoverInstanceHead(
state.deploymentRoot, state.deploymentRoot,
state.captureCommand.request.instanceId, state.captureCommand.request.instanceId,
state.uid, state.uid,
).state, ).state,
'reconciliation_secret_config_applied',
);
assert.equal(
fs.existsSync(path.join(completionRoot, command.request.completionId)),
false,
);
const domainNames = [
'schema_lineage',
'automation',
'secret_and_config',
'run_history',
'plugin_package',
'ai_and_tool',
'identity_policy_audit',
'unknown',
];
const domains = domainNames.map((domain) =>
domain === 'secret_and_config'
? {
domain,
action: 'adapter_required',
evidenceKind: 'secret_config_application',
evidenceDigest: state.applied.applyDigest,
}
: {
domain,
action: 'no_effect',
evidenceKind: 'application_summary',
evidenceDigest: 'a'.repeat(64),
},
);
const receipt = buildLocalReconciliationCompletionReceipt({
completionId: command.request.completionId,
applicationId: command.request.applicationId,
profile: state.captureCommand.request.profile,
instanceId: state.captureCommand.request.instanceId,
cutoverId: state.captureCommand.request.cutoverId,
generation: 1,
activationDigest: state.captureCommand.request.expectedActivationDigest,
applicationPlanDigest: state.application.applicationPlanDigest,
sourceHeadDigest: state.applied.instanceHeadDigest,
domains,
adapterCount: 1,
completedAtMs: command.request.completedAtMs,
});
assert.equal(receipt.schemaVersion, 3);
assert.deepEqual(
normalizeLocalReconciliationCompletionReceipt(receipt),
receipt,
);
const currentHead = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
const durableHead = advanceLocalCutoverInstanceHead(
{
options: { deploymentRoot: state.deploymentRoot },
request: {
cutoverId: state.captureCommand.request.cutoverId,
profile: state.captureCommand.request.profile,
instanceId: state.captureCommand.request.instanceId,
expectedActivationDigest:
state.captureCommand.request.expectedActivationDigest,
requestedAtMs: command.request.completedAtMs,
},
},
state.uid,
'reconciliation_completed', 'reconciliation_completed',
currentHead.generation,
receipt.completionDigest,
); );
assert.equal(durableHead.state, 'reconciliation_completed'); const receipt = JSON.parse(
const selected = localReconciliationSecretConfigApplyPaths( fs.readFileSync(
state.secretConfigApplyRoot, path.join(completionRoot, command.request.completionId, 'receipt.json'),
state.secretConfigId, 'utf8',
); ),
const intent = readLocalReconciliationSecretConfigApplyIntent(
selected,
state.uid,
);
collectLocalReconciliationSecretConfigCompletedStorage(
selected,
intent,
state.uid,
);
collectLocalReconciliationSecretConfigCompletedStorage(
selected,
intent,
state.uid,
);
validateLocalReconciliationSecretConfigCompletedStorage(
selected,
intent,
state.uid,
); );
assert.equal(receipt.schemaVersion, 3);
assert.equal(fs.existsSync(backupPath), false); assert.equal(fs.existsSync(backupPath), false);
assert.deepEqual(fs.readdirSync(backupRoot), []); assert.deepEqual(fs.readdirSync(backupRoot), []);
assert.deepEqual(fs.readdirSync(path.join(applyRoot, 'rollback-work')), []); assert.deepEqual(fs.readdirSync(path.join(applyRoot, 'rollback-work')), []);
@@ -4527,6 +4520,26 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
evidenceDigest: state.applied.applyDigest, evidenceDigest: state.applied.applyDigest,
}, },
); );
for (const domainName of ['identity_policy_audit', 'unknown']) {
assert.equal(
receipt.domains.find((domain) => domain.domain === domainName).action,
'no_effect',
);
}
const verified = await verifyLocalReconciliationCompletion({
schemaVersion: 3,
operation: 'local.deployment.reconciliation.complete.verify',
options: command.options,
request: {
completionId: command.request.completionId,
applicationId: command.request.applicationId,
expectedCompletionDigest: completed.completionDigest,
automation: null,
secretConfig,
runHistory: null,
},
});
assert.equal(verified.status, 'verified');
}); });
test('completion v3 rejects rolled-back Secret/Config evidence', async (t) => { test('completion v3 rejects rolled-back Secret/Config evidence', async (t) => {