@msn-control/liftoff 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPER.md +28 -10
- package/README.md +11 -6
- package/dist/adapters/filesystem/project-transaction.d.ts +2 -1
- package/dist/adapters/filesystem/project-transaction.js +25 -6
- package/dist/adapters/filesystem/project-transaction.js.map +1 -1
- package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
- package/dist/adapters/filesystem/update-previews.d.ts +50 -0
- package/dist/adapters/filesystem/update-previews.js +613 -0
- package/dist/adapters/filesystem/update-previews.js.map +1 -0
- package/dist/adapters/process/shell-command.d.ts +1 -0
- package/dist/adapters/process/shell-command.js +7 -1
- package/dist/adapters/process/shell-command.js.map +1 -1
- package/dist/application/context.d.ts +5 -0
- package/dist/application/diagnose/doctor.js +9 -15
- package/dist/application/diagnose/doctor.js.map +1 -1
- package/dist/application/update/approval.d.ts +39 -0
- package/dist/application/update/approval.js +54 -0
- package/dist/application/update/approval.js.map +1 -0
- package/dist/application/update/command-guidance.d.ts +2 -0
- package/dist/application/update/command-guidance.js +14 -0
- package/dist/application/update/command-guidance.js.map +1 -0
- package/dist/application/update/inspection.d.ts +37 -0
- package/dist/application/update/inspection.js +235 -0
- package/dist/application/update/inspection.js.map +1 -0
- package/dist/application/update/migration-runtime.d.ts +14 -0
- package/dist/application/update/migration-runtime.js +178 -0
- package/dist/application/update/migration-runtime.js.map +1 -0
- package/dist/application/update/output.d.ts +134 -0
- package/dist/application/update/output.js +182 -0
- package/dist/application/update/output.js.map +1 -0
- package/dist/application/update/preview.d.ts +49 -0
- package/dist/application/update/preview.js +220 -0
- package/dist/application/update/preview.js.map +1 -0
- package/dist/application/update/protected-source.d.ts +25 -0
- package/dist/application/update/protected-source.js +81 -0
- package/dist/application/update/protected-source.js.map +1 -0
- package/dist/application/update/reporting.d.ts +4 -4
- package/dist/application/update/reporting.js +14 -13
- package/dist/application/update/reporting.js.map +1 -1
- package/dist/application/update/revalidation-plan.d.ts +22 -0
- package/dist/application/update/revalidation-plan.js +108 -0
- package/dist/application/update/revalidation-plan.js.map +1 -0
- package/dist/application/update/revalidation.d.ts +70 -0
- package/dist/application/update/revalidation.js +337 -0
- package/dist/application/update/revalidation.js.map +1 -0
- package/dist/application/update/review-plan.d.ts +25 -0
- package/dist/application/update/review-plan.js +89 -0
- package/dist/application/update/review-plan.js.map +1 -0
- package/dist/application/update/transaction-approval.d.ts +29 -0
- package/dist/application/update/transaction-approval.js +82 -0
- package/dist/application/update/transaction-approval.js.map +1 -0
- package/dist/application/update/use-case.d.ts +1 -0
- package/dist/application/update/use-case.js +240 -569
- package/dist/application/update/use-case.js.map +1 -1
- package/dist/application/update/write-plan.d.ts +15 -0
- package/dist/application/update/write-plan.js +115 -0
- package/dist/application/update/write-plan.js.map +1 -0
- package/dist/cli/args/definitions.js +5 -4
- package/dist/cli/args/definitions.js.map +1 -1
- package/dist/cli/args/help.js +13 -1
- package/dist/cli/args/help.js.map +1 -1
- package/dist/cli/args/parser.js +22 -7
- package/dist/cli/args/parser.js.map +1 -1
- package/dist/cli/commands/update.js +1 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/domain/governance/policy/identity.d.ts +1 -1
- package/dist/domain/governance/policy/identity.js +2 -2
- package/dist/domain/governance/policy/identity.js.map +1 -1
- package/dist/domain/project/infrastructure-layout.d.ts +1 -1
- package/dist/domain/project/infrastructure-layout.js +2 -1
- package/dist/domain/project/infrastructure-layout.js.map +1 -1
- package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
- package/dist/domain/project/reviewed-update-artifacts.js +3 -0
- package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
- package/dist/generators/common/base.js +5 -1
- package/dist/generators/common/base.js.map +1 -1
- package/dist/governance-activation/activation-state.js +4 -1
- package/dist/governance-activation/activation-state.js.map +1 -1
- package/dist/governance-activation/commands.d.ts +5 -0
- package/dist/governance-activation/commands.js +65 -3
- package/dist/governance-activation/commands.js.map +1 -1
- package/dist/governance-activation/compatibility.d.ts +21 -4
- package/dist/governance-activation/compatibility.js +47 -8
- package/dist/governance-activation/compatibility.js.map +1 -1
- package/dist/governance-activation/doctor.js +32 -5
- package/dist/governance-activation/doctor.js.map +1 -1
- package/dist/governance-activation/historical-state.d.ts +81 -0
- package/dist/governance-activation/historical-state.js +817 -0
- package/dist/governance-activation/historical-state.js.map +1 -0
- package/dist/governance-activation/history-contracts.d.ts +109 -0
- package/dist/governance-activation/history-contracts.js +303 -0
- package/dist/governance-activation/history-contracts.js.map +1 -0
- package/dist/governance-activation/inputs.d.ts +2 -0
- package/dist/governance-activation/inputs.js +13 -1
- package/dist/governance-activation/inputs.js.map +1 -1
- package/dist/governance-activation/migration-history.d.ts +97 -0
- package/dist/governance-activation/migration-history.js +388 -0
- package/dist/governance-activation/migration-history.js.map +1 -0
- package/dist/governance-activation/migration.js +1 -1
- package/dist/governance-activation/migration.js.map +1 -1
- package/dist/governance-activation/proof-records.d.ts +3 -0
- package/dist/governance-activation/proof-records.js +78 -0
- package/dist/governance-activation/proof-records.js.map +1 -0
- package/dist/governance-activation/read-only.d.ts +4 -4
- package/dist/governance-activation/read-only.js +9 -69
- package/dist/governance-activation/read-only.js.map +1 -1
- package/dist/governance-activation/seed-lifecycle.d.ts +28 -6
- package/dist/governance-activation/seed-lifecycle.js +258 -25
- package/dist/governance-activation/seed-lifecycle.js.map +1 -1
- package/dist/governance-activation/transition-planning.d.ts +2 -0
- package/dist/governance-activation/transition-planning.js +12 -3
- package/dist/governance-activation/transition-planning.js.map +1 -1
- package/dist/governance-activation/transitions.d.ts +8 -2
- package/dist/governance-activation/transitions.js +36 -11
- package/dist/governance-activation/transitions.js.map +1 -1
- package/dist/governance-assessment/engine.js +30 -3
- package/dist/governance-assessment/engine.js.map +1 -1
- package/dist/governance-assessment/project.d.ts +7 -0
- package/dist/governance-assessment/project.js +44 -4
- package/dist/governance-assessment/project.js.map +1 -1
- package/dist/repository-governance.js +25 -12
- package/dist/repository-governance.js.map +1 -1
- package/docs/cli-reference.md +101 -28
- package/docs/configuration-and-manifests.md +27 -15
- package/docs/existing-repositories.md +19 -10
- package/docs/repository-governance.md +9 -5
- package/docs/safety-and-consent.md +27 -18
- package/docs/troubleshooting.md +9 -7
- package/docs/workloads.md +5 -4
- package/package.json +1 -1
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canonicalJson, canonicalSha256, isRecord } from '../domain/governance/activation/canonical-json.js';
|
|
4
|
+
import { currentActivationIdentity } from '../domain/governance/activation/graph.js';
|
|
5
|
+
import { errorCode } from '../adapters/filesystem/errors.js';
|
|
6
|
+
import { parseManifest } from '../application/project/manifest.js';
|
|
7
|
+
import { ActivationHistoryError, historicalIdentity, historyArray, historyBoolean, historyCaseKey, historyDigest, historyEnum, historyExact, historyFail, historyLiteral, historyPathKey, historyPathParts, historyRecord, historyRecordId, historyString, historyStrings, historyTimestamp, historicalActivationIdentity, historicalActivationStatePathParts, historicalManifestPathParts, historicalMetadataPathParts, parseHistoryJson, rawHistoryDigest } from './history-contracts.js';
|
|
8
|
+
import { FileSystemError } from '../domain/project/errors.js';
|
|
9
|
+
import { validateGovernanceCompatibilityMetadata } from './compatibility.js';
|
|
10
|
+
// Frozen from 57eba97^:src/governance-activation/{types,validators,graph}.ts.
|
|
11
|
+
// These schemas are historical readers, never current execution validators.
|
|
12
|
+
export const historicalPhaseIds = [
|
|
13
|
+
'seed-valid', 'seed-verified', 'seed-archived', 'committed', 'pushed', 'phase-0-complete',
|
|
14
|
+
'activation-approved', 'credential-ready', 'provider-ready', 'state-path-selected',
|
|
15
|
+
'existing-private-path', 'bootstrap-local', 'runner-ready', 'private-backend-proof',
|
|
16
|
+
'remote-import-verified', 'remote-ready', 'application-foundation', 'workflow-source-ready',
|
|
17
|
+
'dev-proof', 'staging-qualified', 'production-rehearsed', 'green-red-proof',
|
|
18
|
+
'enforcement-approved', 'rulesets-applied', 'live-readback', 'bootstrap-state-disposed'
|
|
19
|
+
];
|
|
20
|
+
const historicalPhaseStates = [
|
|
21
|
+
'pending', 'blocked', 'ready', 'approved', 'running', 'verified', 'failed', 'inapplicable', 'retained', 'disposed'
|
|
22
|
+
];
|
|
23
|
+
const historicalResults = ['verified', 'failed', 'inapplicable', 'retained', 'disposed'];
|
|
24
|
+
const historicalMutationClasses = [
|
|
25
|
+
'none', 'read-worktree', 'write-activation-state', 'write-evidence', 'write-openspec-seed',
|
|
26
|
+
'write-openspec-governance', 'write-local-state', 'delete-local-state', 'write-workflows',
|
|
27
|
+
'write-ruleset-source', 'git-commit', 'git-push', 'github-read', 'github-write',
|
|
28
|
+
'github-secret-write', 'azure-read', 'azure-provider-register', 'azure-network-provision',
|
|
29
|
+
'azure-state-import', 'azure-resource-provision', 'github-ruleset-write'
|
|
30
|
+
];
|
|
31
|
+
const historicalGateKinds = [
|
|
32
|
+
'none', 'repository-publish', 'activation-plan', 'credential-enrollment', 'infrastructure-cost',
|
|
33
|
+
'enforcement', 'destructive-disposal', 'external-blocker'
|
|
34
|
+
];
|
|
35
|
+
const historicalQuestionKinds = [
|
|
36
|
+
'repository-creation-initial-commit-push', 'credential-enrollment',
|
|
37
|
+
'billed-infrastructure-policy-exception-cost-ceiling', 'final-enforcement',
|
|
38
|
+
'destructive-operation', 'external-blocker'
|
|
39
|
+
];
|
|
40
|
+
const destinationTypes = ['local', 'repository', 'subscription', 'environment', 'tenant', 'external'];
|
|
41
|
+
const adapterIds = ['local-evidence', 'selected-spec-workflow', 'git', 'github', 'azure-opentofu', 'local-state'];
|
|
42
|
+
const historicalPhaseGates = {
|
|
43
|
+
'seed-valid': 'none', 'seed-verified': 'none', 'seed-archived': 'none',
|
|
44
|
+
committed: 'repository-publish', pushed: 'repository-publish', 'phase-0-complete': 'none',
|
|
45
|
+
'activation-approved': 'activation-plan', 'credential-ready': 'credential-enrollment',
|
|
46
|
+
'provider-ready': 'infrastructure-cost', 'state-path-selected': 'infrastructure-cost',
|
|
47
|
+
'existing-private-path': 'none', 'bootstrap-local': 'infrastructure-cost',
|
|
48
|
+
'runner-ready': 'infrastructure-cost', 'private-backend-proof': 'none',
|
|
49
|
+
'remote-import-verified': 'infrastructure-cost', 'remote-ready': 'none',
|
|
50
|
+
'application-foundation': 'infrastructure-cost', 'workflow-source-ready': 'none',
|
|
51
|
+
'dev-proof': 'none', 'staging-qualified': 'none', 'production-rehearsed': 'none',
|
|
52
|
+
'green-red-proof': 'none', 'enforcement-approved': 'enforcement', 'rulesets-applied': 'enforcement',
|
|
53
|
+
'live-readback': 'none', 'bootstrap-state-disposed': 'destructive-disposal'
|
|
54
|
+
};
|
|
55
|
+
function phaseId(value, label) {
|
|
56
|
+
return historyEnum(value, historicalPhaseIds, label);
|
|
57
|
+
}
|
|
58
|
+
export function assertHistoricalPhasesComplete(phases) {
|
|
59
|
+
for (const id of historicalPhaseIds) {
|
|
60
|
+
if (phases[id] === undefined)
|
|
61
|
+
historyFail(`phases.${id}`, 'is required.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function reference(value, label) {
|
|
65
|
+
const item = historyExact(value, ['phaseId', 'evidenceId', 'headerDigest', 'result'], label);
|
|
66
|
+
return {
|
|
67
|
+
phaseId: phaseId(item.phaseId, `${label}.phaseId`),
|
|
68
|
+
evidenceId: historyRecordId(item.evidenceId, `${label}.evidenceId`),
|
|
69
|
+
headerDigest: historyDigest(item.headerDigest, `${label}.headerDigest`),
|
|
70
|
+
result: historyEnum(item.result, historicalResults, `${label}.result`)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function retention(value, label) {
|
|
74
|
+
const item = historyExact(value, [
|
|
75
|
+
'status', 'remoteImportEvidenceId', 'remoteImportEvidenceDigest', 'retainedAt', 'disposeAfter',
|
|
76
|
+
'encryptedStatePathParts', 'encryptionKeyPathParts'
|
|
77
|
+
], label, ['disposedAt', 'deletionEvidenceId', 'incompleteCleanup']);
|
|
78
|
+
const status = historyEnum(item.status, ['retained', 'disposed'], `${label}.status`);
|
|
79
|
+
const retainedAt = historyTimestamp(item.retainedAt, `${label}.retainedAt`);
|
|
80
|
+
const disposeAfter = historyTimestamp(item.disposeAfter, `${label}.disposeAfter`);
|
|
81
|
+
if (Date.parse(disposeAfter) - Date.parse(retainedAt) !== 30 * 24 * 60 * 60 * 1000) {
|
|
82
|
+
historyFail(label, 'disposeAfter must be exactly 30 days after retainedAt.');
|
|
83
|
+
}
|
|
84
|
+
if (status === 'disposed' && !Object.hasOwn(item, 'disposedAt'))
|
|
85
|
+
historyFail(label, 'disposedAt is required for disposed state.');
|
|
86
|
+
return {
|
|
87
|
+
status,
|
|
88
|
+
remoteImportEvidenceId: historyRecordId(item.remoteImportEvidenceId, `${label}.remoteImportEvidenceId`),
|
|
89
|
+
remoteImportEvidenceDigest: historyDigest(item.remoteImportEvidenceDigest, `${label}.remoteImportEvidenceDigest`),
|
|
90
|
+
retainedAt, disposeAfter,
|
|
91
|
+
encryptedStatePathParts: historyArray(item.encryptedStatePathParts, `${label}.encryptedStatePathParts`)
|
|
92
|
+
.map((entry) => historyPathParts(entry, `${label}.encryptedStatePathParts`)),
|
|
93
|
+
encryptionKeyPathParts: historyArray(item.encryptionKeyPathParts, `${label}.encryptionKeyPathParts`)
|
|
94
|
+
.map((entry) => historyPathParts(entry, `${label}.encryptionKeyPathParts`)),
|
|
95
|
+
...(Object.hasOwn(item, 'disposedAt') ? { disposedAt: historyTimestamp(item.disposedAt, `${label}.disposedAt`) } : {}),
|
|
96
|
+
...(Object.hasOwn(item, 'deletionEvidenceId') ? { deletionEvidenceId: historyRecordId(item.deletionEvidenceId, `${label}.deletionEvidenceId`) } : {}),
|
|
97
|
+
...(Object.hasOwn(item, 'incompleteCleanup') ? { incompleteCleanup: historyStrings(item.incompleteCleanup, `${label}.incompleteCleanup`) } : {})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function validateHistoricalActivationState(value) {
|
|
101
|
+
const label = 'historicalActivationState';
|
|
102
|
+
const state = historyExact(value, [
|
|
103
|
+
'schemaVersion', 'identity', 'repository', 'activeChange', 'applicability', 'phases', 'createdAt', 'updatedAt'
|
|
104
|
+
], label, ['bootstrapState']);
|
|
105
|
+
historyLiteral(state.schemaVersion, 1, `${label}.schemaVersion`);
|
|
106
|
+
const identity = historicalIdentity(state.identity, `${label}.identity`);
|
|
107
|
+
const repository = historyExact(state.repository, ['id', 'name', 'defaultBranch'], `${label}.repository`);
|
|
108
|
+
const applicability = historyExact(state.applicability, ['statePath', 'privateStagingDast', 'credentialRequired'], `${label}.applicability`);
|
|
109
|
+
let activeChange = null;
|
|
110
|
+
if (state.activeChange !== null) {
|
|
111
|
+
const change = historyExact(state.activeChange, ['id', 'kind'], `${label}.activeChange`);
|
|
112
|
+
activeChange = {
|
|
113
|
+
id: historyRecordId(change.id, `${label}.activeChange.id`),
|
|
114
|
+
kind: historyEnum(change.kind, ['openspec', 'spec-kit'], `${label}.activeChange.kind`)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const rawPhases = historyExact(state.phases, historicalPhaseIds, `${label}.phases`);
|
|
118
|
+
const phases = {};
|
|
119
|
+
for (const id of historicalPhaseIds) {
|
|
120
|
+
const at = `${label}.phases.${id}`;
|
|
121
|
+
const phase = historyExact(rawPhases[id], ['state', 'updatedAt', 'evidence', 'approvals', 'blockers'], at);
|
|
122
|
+
const evidence = historyArray(phase.evidence, `${at}.evidence`).map((entry, index) => reference(entry, `${at}.evidence[${index}]`));
|
|
123
|
+
if (evidence.some((entry) => entry.phaseId !== id))
|
|
124
|
+
historyFail(at, 'evidence reference names another phase.');
|
|
125
|
+
const approvals = historyArray(phase.approvals, `${at}.approvals`).map((entry) => historyRecordId(entry, `${at}.approvals`));
|
|
126
|
+
if (new Set(evidence.map((entry) => entry.evidenceId)).size !== evidence.length || new Set(approvals).size !== approvals.length) {
|
|
127
|
+
historyFail(at, 'contains duplicate evidence or approval references.');
|
|
128
|
+
}
|
|
129
|
+
phases[id] = {
|
|
130
|
+
state: historyEnum(phase.state, historicalPhaseStates, `${at}.state`),
|
|
131
|
+
updatedAt: historyString(phase.updatedAt, `${at}.updatedAt`),
|
|
132
|
+
evidence, approvals, blockers: historyStrings(phase.blockers, `${at}.blockers`)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
assertHistoricalPhasesComplete(phases);
|
|
136
|
+
return {
|
|
137
|
+
schemaVersion: 1, identity,
|
|
138
|
+
repository: {
|
|
139
|
+
id: historyString(repository.id, `${label}.repository.id`),
|
|
140
|
+
name: historyString(repository.name, `${label}.repository.name`),
|
|
141
|
+
defaultBranch: historyString(repository.defaultBranch, `${label}.repository.defaultBranch`)
|
|
142
|
+
},
|
|
143
|
+
activeChange,
|
|
144
|
+
applicability: {
|
|
145
|
+
statePath: historyEnum(applicability.statePath, ['existing-private', 'bootstrap-local', 'none'], `${label}.applicability.statePath`),
|
|
146
|
+
privateStagingDast: historyBoolean(applicability.privateStagingDast, `${label}.applicability.privateStagingDast`),
|
|
147
|
+
credentialRequired: historyBoolean(applicability.credentialRequired, `${label}.applicability.credentialRequired`)
|
|
148
|
+
},
|
|
149
|
+
...(Object.hasOwn(state, 'bootstrapState') ? { bootstrapState: retention(state.bootstrapState, `${label}.bootstrapState`) } : {}),
|
|
150
|
+
phases, createdAt: historyString(state.createdAt, `${label}.createdAt`),
|
|
151
|
+
updatedAt: historyString(state.updatedAt, `${label}.updatedAt`)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function transition(value, label) {
|
|
155
|
+
const item = historyExact(value, ['phaseId', 'baselineSha', 'inputDigest', 'transitionDigest'], label);
|
|
156
|
+
return {
|
|
157
|
+
phaseId: phaseId(item.phaseId, `${label}.phaseId`),
|
|
158
|
+
baselineSha: historyDigest(item.baselineSha, `${label}.baselineSha`),
|
|
159
|
+
inputDigest: historyDigest(item.inputDigest, `${label}.inputDigest`),
|
|
160
|
+
transitionDigest: historyDigest(item.transitionDigest, `${label}.transitionDigest`)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function historicalProofIdentity(item, label) {
|
|
164
|
+
const identity = historicalIdentity(item.identity, `${label}.identity`);
|
|
165
|
+
const graphHash = historyDigest(item.phaseGraphHash, `${label}.phaseGraphHash`);
|
|
166
|
+
if (graphHash !== identity.phaseGraphHash)
|
|
167
|
+
historyFail(label, 'graph hash contradicts the historical identity.');
|
|
168
|
+
const id = phaseId(item.phaseId, `${label}.phaseId`);
|
|
169
|
+
const baselineSha = historyDigest(item.baselineSha, `${label}.baselineSha`);
|
|
170
|
+
const inputDigest = historyDigest(item.inputDigest, `${label}.inputDigest`);
|
|
171
|
+
const proofTransition = transition(item.transition, `${label}.transition`);
|
|
172
|
+
if (proofTransition.phaseId !== id || proofTransition.baselineSha !== baselineSha || proofTransition.inputDigest !== inputDigest) {
|
|
173
|
+
historyFail(label, 'transition contradicts the proof phase, baseline or inputs.');
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
schemaVersion: historyLiteral(item.schemaVersion, 1, `${label}.schemaVersion`),
|
|
177
|
+
identity, phaseGraphHash: graphHash, phaseId: id, baselineSha, inputDigest, transition: proofTransition,
|
|
178
|
+
repositoryId: historyString(item.repositoryId, `${label}.repositoryId`)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function validateHistoricalEvidenceHeader(value) {
|
|
182
|
+
const label = 'historicalEvidenceHeader';
|
|
183
|
+
const item = historyExact(value, [
|
|
184
|
+
'schemaVersion', 'repositoryId', 'identity', 'phaseGraphHash', 'phaseId', 'phaseContractDigest',
|
|
185
|
+
'inputDigest', 'baselineSha', 'transition', 'producedAt', 'producer', 'result'
|
|
186
|
+
], label);
|
|
187
|
+
return {
|
|
188
|
+
...historicalProofIdentity(item, label),
|
|
189
|
+
phaseContractDigest: historyDigest(item.phaseContractDigest, `${label}.phaseContractDigest`),
|
|
190
|
+
producedAt: historyTimestamp(item.producedAt, `${label}.producedAt`),
|
|
191
|
+
producer: historyString(item.producer, `${label}.producer`),
|
|
192
|
+
result: historyEnum(item.result, historicalResults, `${label}.result`)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export function validateHistoricalLiveReadback(value) {
|
|
196
|
+
const label = 'historicalLiveReadback';
|
|
197
|
+
const item = historyExact(value, [
|
|
198
|
+
'schemaVersion', 'repositoryId', 'identity', 'phaseGraphHash', 'phaseId', 'baselineSha', 'inputDigest',
|
|
199
|
+
'transition', 'observedAt', 'provider', 'resourceType', 'resourceId', 'sourceDigest', 'readbackDigest', 'matches'
|
|
200
|
+
], label);
|
|
201
|
+
return {
|
|
202
|
+
...historicalProofIdentity(item, label),
|
|
203
|
+
observedAt: historyTimestamp(item.observedAt, `${label}.observedAt`),
|
|
204
|
+
provider: historyEnum(item.provider, ['github', 'azure'], `${label}.provider`),
|
|
205
|
+
resourceType: historyString(item.resourceType, `${label}.resourceType`),
|
|
206
|
+
resourceId: historyString(item.resourceId, `${label}.resourceId`),
|
|
207
|
+
sourceDigest: historyDigest(item.sourceDigest, `${label}.sourceDigest`),
|
|
208
|
+
readbackDigest: historyDigest(item.readbackDigest, `${label}.readbackDigest`),
|
|
209
|
+
matches: historyBoolean(item.matches, `${label}.matches`)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function validateEmbeddedPaths(value, label) {
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
value.forEach((entry, index) => validateEmbeddedPaths(entry, `${label}[${index}]`));
|
|
215
|
+
}
|
|
216
|
+
else if (isRecord(value)) {
|
|
217
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
218
|
+
if (key === 'pathParts' || key.endsWith('PathParts')) {
|
|
219
|
+
const parts = historyArray(entry, `${label}.${key}`);
|
|
220
|
+
if (parts.length === 0 && ['cwdPathParts', 'encryptedStatePathParts', 'encryptionKeyPathParts', 'deletedPathParts'].includes(key))
|
|
221
|
+
continue;
|
|
222
|
+
if (parts.length > 0 && Array.isArray(parts[0]))
|
|
223
|
+
parts.forEach((item) => historyPathParts(item, `${label}.${key}`));
|
|
224
|
+
else
|
|
225
|
+
historyPathParts(parts, `${label}.${key}`);
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
validateEmbeddedPaths(entry, `${label}.${key}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function validateHistoricalEvidenceRecord(value, headerOnlyEvidenceId) {
|
|
233
|
+
const label = 'historicalEvidenceRecord';
|
|
234
|
+
const item = historyRecord(value, label);
|
|
235
|
+
// Header-only JSON was an explicit v1 format in commands.ts, not an unversioned import.
|
|
236
|
+
if (!Object.hasOwn(item, 'header')) {
|
|
237
|
+
return {
|
|
238
|
+
evidenceId: historyRecordId(headerOnlyEvidenceId, `${label}.registeredHeaderOnlyEvidenceId`),
|
|
239
|
+
header: validateHistoricalEvidenceHeader(item)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
historyExact(item, ['evidenceId', 'header'], label, ['payload', 'liveReadback']);
|
|
243
|
+
if (Object.hasOwn(item, 'payload'))
|
|
244
|
+
validateEmbeddedPaths(item.payload, `${label}.payload`);
|
|
245
|
+
const header = validateHistoricalEvidenceHeader(item.header);
|
|
246
|
+
const liveReadback = Object.hasOwn(item, 'liveReadback')
|
|
247
|
+
? historyArray(item.liveReadback, `${label}.liveReadback`).map(validateHistoricalLiveReadback) : undefined;
|
|
248
|
+
if (liveReadback?.some((proof) => proof.repositoryId !== header.repositoryId || proof.phaseId !== header.phaseId ||
|
|
249
|
+
canonicalSha256(proof.transition) !== canonicalSha256(header.transition))) {
|
|
250
|
+
historyFail(label, 'live readback contradicts its enclosing evidence record.');
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
evidenceId: historyRecordId(item.evidenceId, `${label}.evidenceId`), header,
|
|
254
|
+
...(Object.hasOwn(item, 'payload') ? { payload: item.payload } : {}),
|
|
255
|
+
...(liveReadback === undefined ? {} : { liveReadback })
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function uniqueSorted(values, key, label) {
|
|
259
|
+
if (new Set(values.map(key)).size !== values.length)
|
|
260
|
+
historyFail(label, 'contains duplicate scope entries.');
|
|
261
|
+
return values.sort((a, b) => key(a).localeCompare(key(b), 'en'));
|
|
262
|
+
}
|
|
263
|
+
function nullableString(value, label) {
|
|
264
|
+
return value === null ? null : historyString(value, label);
|
|
265
|
+
}
|
|
266
|
+
export function validateHistoricalApprovalEnvelope(value) {
|
|
267
|
+
const label = 'historicalApprovalEnvelope';
|
|
268
|
+
const item = historyExact(value, [
|
|
269
|
+
'schemaVersion', 'id', 'phaseId', 'gateKind', 'identity', 'baselineSha', 'planDigest', 'resources',
|
|
270
|
+
'destinations', 'permissions', 'costCeiling', 'policyExceptions', 'destructiveScope', 'expiresAt', 'approvedAt', 'approver'
|
|
271
|
+
], label);
|
|
272
|
+
const id = phaseId(item.phaseId, `${label}.phaseId`);
|
|
273
|
+
const gateKind = historyEnum(item.gateKind, historicalGateKinds, `${label}.gateKind`);
|
|
274
|
+
if (gateKind !== historicalPhaseGates[id])
|
|
275
|
+
historyFail(label, 'gate does not match its historical phase.');
|
|
276
|
+
const resources = historyArray(item.resources, `${label}.resources`).map((entry, index) => {
|
|
277
|
+
const at = `${label}.resources[${index}]`;
|
|
278
|
+
const resource = historyExact(entry, ['type', 'identity'], at);
|
|
279
|
+
return { type: historyString(resource.type, `${at}.type`).trim().toLowerCase(), identity: historyString(resource.identity, `${at}.identity`).trim() };
|
|
280
|
+
});
|
|
281
|
+
const destinations = historyArray(item.destinations, `${label}.destinations`).map((entry, index) => {
|
|
282
|
+
const at = `${label}.destinations[${index}]`;
|
|
283
|
+
const destination = historyExact(entry, ['type', 'identity', 'repository', 'subscriptionId'], at);
|
|
284
|
+
return {
|
|
285
|
+
type: historyEnum(destination.type, destinationTypes, `${at}.type`),
|
|
286
|
+
identity: historyString(destination.identity, `${at}.identity`).trim(),
|
|
287
|
+
repository: nullableString(destination.repository, `${at}.repository`)?.trim() ?? null,
|
|
288
|
+
subscriptionId: nullableString(destination.subscriptionId, `${at}.subscriptionId`)?.trim() ?? null
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const cost = historyExact(item.costCeiling, ['currency', 'fixedMonthlyCents', 'usageMonthlyCents'], `${label}.costCeiling`);
|
|
292
|
+
const currency = historyString(cost.currency, `${label}.costCeiling.currency`).trim();
|
|
293
|
+
if (!/^[A-Z]{3}$/u.test(currency))
|
|
294
|
+
historyFail(label, 'cost currency must be a three-letter uppercase ISO currency.');
|
|
295
|
+
const cents = (value, at) => {
|
|
296
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
297
|
+
historyFail(at, 'must be non-negative safe integer cents.');
|
|
298
|
+
return value;
|
|
299
|
+
};
|
|
300
|
+
const scopeStrings = (value, at, lower = false) => uniqueSorted(historyStrings(value, at).map((entry) => lower ? entry.trim().toLowerCase() : entry.trim()), (entry) => entry, at);
|
|
301
|
+
return {
|
|
302
|
+
schemaVersion: historyLiteral(item.schemaVersion, 1, `${label}.schemaVersion`),
|
|
303
|
+
id: historyRecordId(item.id, `${label}.id`), phaseId: id, gateKind,
|
|
304
|
+
identity: historicalIdentity(item.identity, `${label}.identity`),
|
|
305
|
+
baselineSha: historyDigest(item.baselineSha, `${label}.baselineSha`),
|
|
306
|
+
planDigest: historyDigest(item.planDigest, `${label}.planDigest`),
|
|
307
|
+
resources: uniqueSorted(resources, canonicalJson, `${label}.resources`),
|
|
308
|
+
destinations: uniqueSorted(destinations, canonicalJson, `${label}.destinations`),
|
|
309
|
+
permissions: scopeStrings(item.permissions, `${label}.permissions`, true),
|
|
310
|
+
costCeiling: {
|
|
311
|
+
currency, fixedMonthlyCents: cents(cost.fixedMonthlyCents, `${label}.costCeiling.fixedMonthlyCents`),
|
|
312
|
+
usageMonthlyCents: cents(cost.usageMonthlyCents, `${label}.costCeiling.usageMonthlyCents`)
|
|
313
|
+
},
|
|
314
|
+
policyExceptions: scopeStrings(item.policyExceptions, `${label}.policyExceptions`),
|
|
315
|
+
destructiveScope: scopeStrings(item.destructiveScope, `${label}.destructiveScope`),
|
|
316
|
+
expiresAt: historyTimestamp(item.expiresAt, `${label}.expiresAt`),
|
|
317
|
+
approvedAt: historyTimestamp(item.approvedAt, `${label}.approvedAt`),
|
|
318
|
+
approver: historyString(item.approver, `${label}.approver`)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function historicalApprovalEnvelopeHash(envelope) {
|
|
322
|
+
const { id: _id, approvedAt: _approvedAt, approver: _approver, ...scope } = validateHistoricalApprovalEnvelope(envelope);
|
|
323
|
+
return canonicalSha256(scope);
|
|
324
|
+
}
|
|
325
|
+
function operationDestination(value, label) {
|
|
326
|
+
const item = historyExact(value, ['type', 'identity'], label, ['pathParts', 'repository', 'subscriptionId', 'ref']);
|
|
327
|
+
return {
|
|
328
|
+
type: historyEnum(item.type, destinationTypes, `${label}.type`),
|
|
329
|
+
identity: historyString(item.identity, `${label}.identity`),
|
|
330
|
+
...(Object.hasOwn(item, 'pathParts') ? { pathParts: historyPathParts(item.pathParts, `${label}.pathParts`) } : {}),
|
|
331
|
+
...(Object.hasOwn(item, 'repository') ? { repository: historyString(item.repository, `${label}.repository`) } : {}),
|
|
332
|
+
...(Object.hasOwn(item, 'subscriptionId') ? { subscriptionId: historyString(item.subscriptionId, `${label}.subscriptionId`) } : {}),
|
|
333
|
+
...(Object.hasOwn(item, 'ref') ? { ref: historyString(item.ref, `${label}.ref`) } : {})
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function operation(value, label) {
|
|
337
|
+
const item = historyExact(value, ['adapter', 'actionId', 'mutationClass', 'phaseId', 'inputs', 'destination', 'remote', 'destructive'], label);
|
|
338
|
+
const inputs = historyRecord(item.inputs, `${label}.inputs`);
|
|
339
|
+
validateEmbeddedPaths(inputs, `${label}.inputs`);
|
|
340
|
+
return {
|
|
341
|
+
adapter: historyEnum(item.adapter, adapterIds, `${label}.adapter`),
|
|
342
|
+
actionId: historyString(item.actionId, `${label}.actionId`),
|
|
343
|
+
mutationClass: historyEnum(item.mutationClass, historicalMutationClasses, `${label}.mutationClass`),
|
|
344
|
+
phaseId: phaseId(item.phaseId, `${label}.phaseId`), inputs,
|
|
345
|
+
destination: operationDestination(item.destination, `${label}.destination`),
|
|
346
|
+
remote: historyBoolean(item.remote, `${label}.remote`), destructive: historyBoolean(item.destructive, `${label}.destructive`)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function evaluation(value, label) {
|
|
350
|
+
const item = historyExact(value, [
|
|
351
|
+
'phaseId', 'gateKind', 'questionKind', 'approvalRequired', 'status', 'envelopeId', 'envelopeHash', 'reasons', 'expansionReasons'
|
|
352
|
+
], label);
|
|
353
|
+
return {
|
|
354
|
+
phaseId: phaseId(item.phaseId, `${label}.phaseId`),
|
|
355
|
+
gateKind: historyEnum(item.gateKind, historicalGateKinds, `${label}.gateKind`),
|
|
356
|
+
questionKind: item.questionKind === null ? null : historyEnum(item.questionKind, historicalQuestionKinds, `${label}.questionKind`),
|
|
357
|
+
approvalRequired: historyBoolean(item.approvalRequired, `${label}.approvalRequired`),
|
|
358
|
+
status: historyEnum(item.status, ['not-required', 'approval-required', 'reused', 'expired', 'invalidated'], `${label}.status`),
|
|
359
|
+
envelopeId: item.envelopeId === null ? null : historyRecordId(item.envelopeId, `${label}.envelopeId`),
|
|
360
|
+
envelopeHash: item.envelopeHash === null ? null : historyDigest(item.envelopeHash, `${label}.envelopeHash`),
|
|
361
|
+
reasons: historyStrings(item.reasons, `${label}.reasons`), expansionReasons: historyStrings(item.expansionReasons, `${label}.expansionReasons`)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function rollbackPlan(value, label) {
|
|
365
|
+
const item = historyExact(value, ['phaseId', 'strategy', 'target', 'operations', 'retained', 'cleanupWarnings'], label);
|
|
366
|
+
return {
|
|
367
|
+
phaseId: phaseId(item.phaseId, `${label}.phaseId`),
|
|
368
|
+
strategy: historyEnum(item.strategy, ['none', 'retain', 'reverse-to', 'dispose'], `${label}.strategy`),
|
|
369
|
+
target: item.target === null ? null : phaseId(item.target, `${label}.target`),
|
|
370
|
+
operations: historyArray(item.operations, `${label}.operations`).map((entry, index) => operation(entry, `${label}.operations[${index}]`)),
|
|
371
|
+
retained: historyStrings(item.retained, `${label}.retained`), cleanupWarnings: historyStrings(item.cleanupWarnings, `${label}.cleanupWarnings`)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
export function validateHistoricalSavedTransitionPlan(value) {
|
|
375
|
+
const label = 'historicalTransitionPlan';
|
|
376
|
+
const item = historyExact(value, [
|
|
377
|
+
'schemaVersion', 'phaseId', 'createdAt', 'expiresAt', 'identity', 'graphHash', 'stateHash',
|
|
378
|
+
'baselineDigest', 'inputDigest', 'transitionDigest', 'planDigest', 'mutationClasses', 'operations',
|
|
379
|
+
'approval', 'rollbackPlan', 'noSecrets'
|
|
380
|
+
], label);
|
|
381
|
+
const identity = historicalIdentity(item.identity, `${label}.identity`);
|
|
382
|
+
const graphHash = historyDigest(item.graphHash, `${label}.graphHash`);
|
|
383
|
+
if (graphHash !== identity.phaseGraphHash)
|
|
384
|
+
historyFail(label, 'graphHash contradicts the historical identity.');
|
|
385
|
+
const id = phaseId(item.phaseId, `${label}.phaseId`);
|
|
386
|
+
const operations = historyArray(item.operations, `${label}.operations`).map((entry, index) => operation(entry, `${label}.operations[${index}]`));
|
|
387
|
+
if (operations.some((entry) => entry.phaseId !== id))
|
|
388
|
+
historyFail(label, 'operation phase differs from its plan.');
|
|
389
|
+
const mutations = historyExact(item.mutationClasses, ['local', 'remote'], `${label}.mutationClasses`);
|
|
390
|
+
const approval = historyExact(item.approval, ['gateKind', 'required', 'evaluation', 'envelopeId', 'envelopeHash'], `${label}.approval`);
|
|
391
|
+
const approvalEvaluation = evaluation(approval.evaluation, `${label}.approval.evaluation`);
|
|
392
|
+
const gateKind = historyEnum(approval.gateKind, historicalGateKinds, `${label}.approval.gateKind`);
|
|
393
|
+
const required = historyBoolean(approval.required, `${label}.approval.required`);
|
|
394
|
+
const envelopeId = approval.envelopeId === null ? null : historyRecordId(approval.envelopeId, `${label}.approval.envelopeId`);
|
|
395
|
+
const envelopeHash = approval.envelopeHash === null ? null : historyDigest(approval.envelopeHash, `${label}.approval.envelopeHash`);
|
|
396
|
+
if (gateKind !== historicalPhaseGates[id] || required !== (gateKind !== 'none') ||
|
|
397
|
+
approvalEvaluation.phaseId !== id || approvalEvaluation.gateKind !== gateKind ||
|
|
398
|
+
approvalEvaluation.envelopeId !== envelopeId || approvalEvaluation.envelopeHash !== envelopeHash ||
|
|
399
|
+
(envelopeId === null) !== (envelopeHash === null)) {
|
|
400
|
+
historyFail(label, 'approval references or gate contradict the historical phase.');
|
|
401
|
+
}
|
|
402
|
+
const rollback = rollbackPlan(item.rollbackPlan, `${label}.rollbackPlan`);
|
|
403
|
+
if (rollback.phaseId !== id)
|
|
404
|
+
historyFail(label, 'rollback names another phase.');
|
|
405
|
+
return {
|
|
406
|
+
schemaVersion: historyLiteral(item.schemaVersion, 1, `${label}.schemaVersion`), phaseId: id,
|
|
407
|
+
createdAt: historyTimestamp(item.createdAt, `${label}.createdAt`),
|
|
408
|
+
expiresAt: historyTimestamp(item.expiresAt, `${label}.expiresAt`), identity, graphHash,
|
|
409
|
+
stateHash: item.stateHash === null ? null : historyDigest(item.stateHash, `${label}.stateHash`),
|
|
410
|
+
baselineDigest: historyDigest(item.baselineDigest, `${label}.baselineDigest`),
|
|
411
|
+
inputDigest: historyDigest(item.inputDigest, `${label}.inputDigest`),
|
|
412
|
+
transitionDigest: historyDigest(item.transitionDigest, `${label}.transitionDigest`),
|
|
413
|
+
planDigest: historyDigest(item.planDigest, `${label}.planDigest`),
|
|
414
|
+
mutationClasses: {
|
|
415
|
+
local: historyArray(mutations.local, `${label}.mutationClasses.local`).map((entry) => historyEnum(entry, historicalMutationClasses, `${label}.mutationClasses.local`)),
|
|
416
|
+
remote: historyArray(mutations.remote, `${label}.mutationClasses.remote`).map((entry) => historyEnum(entry, historicalMutationClasses, `${label}.mutationClasses.remote`))
|
|
417
|
+
},
|
|
418
|
+
operations, approval: { gateKind, required, evaluation: approvalEvaluation, envelopeId, envelopeHash },
|
|
419
|
+
rollbackPlan: rollback, noSecrets: historyLiteral(item.noSecrets, true, `${label}.noSecrets`)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
export function historicalTransitionPlanPathParts(plan) {
|
|
423
|
+
return historyPathParts([
|
|
424
|
+
'governance', 'plans', `${plan.phaseId}-${plan.createdAt.replace(/[^0-9A-Za-z]/g, '')}-${plan.planDigest.slice(0, 12)}.json`
|
|
425
|
+
], 'historical transition plan path');
|
|
426
|
+
}
|
|
427
|
+
export async function resolveHistoryProjectPath(projectRoot, rawParts) {
|
|
428
|
+
const parts = historyPathParts(rawParts, 'history path');
|
|
429
|
+
const root = path.resolve(projectRoot);
|
|
430
|
+
const rootDetails = await lstat(root);
|
|
431
|
+
if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink())
|
|
432
|
+
historyFail(root, 'project root must be a real directory.', 'unsafe-history-path');
|
|
433
|
+
const canonicalRoot = await realpath(root);
|
|
434
|
+
let cursor = canonicalRoot;
|
|
435
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
436
|
+
let names;
|
|
437
|
+
try {
|
|
438
|
+
names = await readdir(cursor);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (errorCode(error) !== 'ENOENT')
|
|
442
|
+
throw error;
|
|
443
|
+
return path.join(canonicalRoot, ...parts);
|
|
444
|
+
}
|
|
445
|
+
const part = parts[index];
|
|
446
|
+
const matches = names.filter((name) => historyCaseKey([name]) === historyCaseKey([part]));
|
|
447
|
+
if (matches.length > 1 || matches.length === 1 && matches[0] !== part) {
|
|
448
|
+
historyFail(historyPathKey(parts), `case-colliding path segment ${JSON.stringify(part)}.`, 'history-path-collision');
|
|
449
|
+
}
|
|
450
|
+
cursor = path.join(cursor, part);
|
|
451
|
+
let details;
|
|
452
|
+
try {
|
|
453
|
+
details = await lstat(cursor);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (errorCode(error) !== 'ENOENT')
|
|
457
|
+
throw error;
|
|
458
|
+
return path.join(canonicalRoot, ...parts);
|
|
459
|
+
}
|
|
460
|
+
if (details.isSymbolicLink() || details.isFile() && details.nlink !== 1) {
|
|
461
|
+
historyFail(historyPathKey(parts), 'symbolic links, junctions and hard-linked files are not historical migration targets.', 'unsafe-history-path');
|
|
462
|
+
}
|
|
463
|
+
if (index < parts.length - 1 && !details.isDirectory()) {
|
|
464
|
+
historyFail(historyPathKey(parts), 'a parent is not a directory.', 'unsafe-history-path');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return cursor;
|
|
468
|
+
}
|
|
469
|
+
export async function captureHistoryFile(projectRoot, parts) {
|
|
470
|
+
const pathParts = historyPathParts(parts, 'history file path');
|
|
471
|
+
const target = await resolveHistoryProjectPath(projectRoot, pathParts);
|
|
472
|
+
let details;
|
|
473
|
+
try {
|
|
474
|
+
details = await lstat(target);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (errorCode(error) !== 'ENOENT')
|
|
478
|
+
throw error;
|
|
479
|
+
return { pathParts };
|
|
480
|
+
}
|
|
481
|
+
if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1) {
|
|
482
|
+
historyFail(historyPathKey(pathParts), 'must be a regular unlinked file.', 'unsafe-history-path');
|
|
483
|
+
}
|
|
484
|
+
const content = await readFile(target);
|
|
485
|
+
const after = await lstat(target);
|
|
486
|
+
if (after.ino !== details.ino || after.dev !== details.dev || after.mode !== details.mode ||
|
|
487
|
+
after.size !== details.size || after.mtimeMs !== details.mtimeMs || after.nlink !== 1) {
|
|
488
|
+
historyFail(historyPathKey(pathParts), 'changed while being read; obtain a fresh preview.', 'historical-source-changed');
|
|
489
|
+
}
|
|
490
|
+
return { pathParts, content, mode: details.mode & 0o7777 };
|
|
491
|
+
}
|
|
492
|
+
export function validateHistoricalSourceManifest(value) {
|
|
493
|
+
const label = 'historicalSourceManifest';
|
|
494
|
+
const raw = historyRecord(value, label);
|
|
495
|
+
historyLiteral(raw.artifactVersion, 7, `${label}.artifactVersion`);
|
|
496
|
+
const governance = historyRecord(raw.governance, `${label}.governance`);
|
|
497
|
+
historicalIdentity(governance.activationIdentity, `${label}.governance.activationIdentity`);
|
|
498
|
+
try {
|
|
499
|
+
return parseManifest(raw);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
if (!(error instanceof FileSystemError))
|
|
503
|
+
throw error;
|
|
504
|
+
return historyFail(label, error.message, 'invalid-historical-manifest');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function validateHistoricalCompatibility(value, label) {
|
|
508
|
+
const item = historyExact(value, [
|
|
509
|
+
'schemaVersion', 'generatedBy', 'liftoffVersion', 'minimumLiftoffVersions', 'manifest', 'activation', 'managedCore'
|
|
510
|
+
], label);
|
|
511
|
+
historyLiteral(item.schemaVersion, 1, `${label}.schemaVersion`);
|
|
512
|
+
historyLiteral(item.generatedBy, 'Mission Control Liftoff', `${label}.generatedBy`);
|
|
513
|
+
historyLiteral(item.liftoffVersion, historicalActivationIdentity.liftoffVersion, `${label}.liftoffVersion`);
|
|
514
|
+
const minimum = historyExact(item.minimumLiftoffVersions, ['manifestWriteVersion7', 'remedy'], `${label}.minimumLiftoffVersions`);
|
|
515
|
+
historyLiteral(minimum.manifestWriteVersion7, '0.10.0', `${label}.minimumLiftoffVersions.manifestWriteVersion7`);
|
|
516
|
+
historyString(minimum.remedy, `${label}.minimumLiftoffVersions.remedy`);
|
|
517
|
+
const manifest = historyExact(item.manifest, ['readVersions', 'writeVersion', 'hashAuthority'], `${label}.manifest`);
|
|
518
|
+
if (canonicalJson(manifest.readVersions) !== canonicalJson([2, 3, 4, 5, 6, 7]))
|
|
519
|
+
historyFail(label, 'has an unknown historical manifest reader declaration.');
|
|
520
|
+
historyLiteral(manifest.writeVersion, 7, `${label}.manifest.writeVersion`);
|
|
521
|
+
historyLiteral(manifest.hashAuthority, 'liftoff.manifest.json managedArtifacts[].contentHash', `${label}.manifest.hashAuthority`);
|
|
522
|
+
const activation = historyExact(item.activation, [
|
|
523
|
+
'currentCompatibleTuples', 'recognizedGraphHashes', 'graphMappings', 'historicalStateMigrations', 'unsupportedRemedy'
|
|
524
|
+
], `${label}.activation`);
|
|
525
|
+
const tuples = historyArray(activation.currentCompatibleTuples, `${label}.activation.currentCompatibleTuples`);
|
|
526
|
+
if (tuples.length !== 1)
|
|
527
|
+
historyFail(label, 'requires the single registered historical activation tuple.');
|
|
528
|
+
historicalIdentity(tuples[0], `${label}.activation.currentCompatibleTuples[0]`);
|
|
529
|
+
if (canonicalJson(activation.recognizedGraphHashes) !== canonicalJson([historicalActivationIdentity.phaseGraphHash]) ||
|
|
530
|
+
canonicalJson(activation.graphMappings) !== '[]\n' || canonicalJson(activation.historicalStateMigrations) !== '[]\n') {
|
|
531
|
+
historyFail(label, 'contains an unknown historical graph or migration declaration.');
|
|
532
|
+
}
|
|
533
|
+
historyString(activation.unsupportedRemedy, `${label}.activation.unsupportedRemedy`);
|
|
534
|
+
const core = historyExact(item.managedCore, ['logicalNameAllowlist', 'pathAllowlist', 'updateInventory', 'validation'], `${label}.managedCore`);
|
|
535
|
+
const logicalNames = historyStrings(core.logicalNameAllowlist, `${label}.managedCore.logicalNameAllowlist`);
|
|
536
|
+
const paths = historyArray(core.pathAllowlist, `${label}.managedCore.pathAllowlist`).map((entry) => historyPathParts(entry, `${label}.managedCore.pathAllowlist`));
|
|
537
|
+
const entries = historyArray(core.updateInventory, `${label}.managedCore.updateInventory`).map((entry, index) => {
|
|
538
|
+
const at = `${label}.managedCore.updateInventory[${index}]`;
|
|
539
|
+
const file = historyExact(entry, ['logicalName', 'pathParts', 'lifecycle', 'contentHashAuthority'], at);
|
|
540
|
+
historyLiteral(file.lifecycle, 'managed-core', `${at}.lifecycle`);
|
|
541
|
+
historyLiteral(file.contentHashAuthority, 'liftoff.manifest.json managedArtifacts[].contentHash', `${at}.contentHashAuthority`);
|
|
542
|
+
return { logicalName: historyString(file.logicalName, `${at}.logicalName`), pathParts: historyPathParts(file.pathParts, `${at}.pathParts`) };
|
|
543
|
+
});
|
|
544
|
+
if (new Set(logicalNames).size !== logicalNames.length ||
|
|
545
|
+
new Set(paths.map(historyCaseKey)).size !== paths.length ||
|
|
546
|
+
new Set(entries.map((entry) => entry.logicalName)).size !== entries.length ||
|
|
547
|
+
new Set(entries.map((entry) => historyCaseKey(entry.pathParts))).size !== entries.length ||
|
|
548
|
+
entries.length !== paths.length ||
|
|
549
|
+
entries.some((entry) => !logicalNames.includes(entry.logicalName) || !paths.some((parts) => historyPathKey(parts) === historyPathKey(entry.pathParts)))) {
|
|
550
|
+
historyFail(label, 'managed inventory and exact allowlists disagree.');
|
|
551
|
+
}
|
|
552
|
+
const validation = historyExact(core.validation, ['strictJson', 'crossPlatformPathParts', 'noSetupSkillVersion', 'checkModeWritesBytes'], `${label}.managedCore.validation`);
|
|
553
|
+
historyLiteral(validation.strictJson, true, `${label}.managedCore.validation.strictJson`);
|
|
554
|
+
historyLiteral(validation.crossPlatformPathParts, true, `${label}.managedCore.validation.crossPlatformPathParts`);
|
|
555
|
+
historyLiteral(validation.noSetupSkillVersion, true, `${label}.managedCore.validation.noSetupSkillVersion`);
|
|
556
|
+
historyLiteral(validation.checkModeWritesBytes, 0, `${label}.managedCore.validation.checkModeWritesBytes`);
|
|
557
|
+
}
|
|
558
|
+
function validateHistoricalMetadata(file) {
|
|
559
|
+
const label = historyPathKey(file.pathParts);
|
|
560
|
+
if (!label.endsWith('.json'))
|
|
561
|
+
return;
|
|
562
|
+
const value = parseHistoryJson(file.content, label);
|
|
563
|
+
// Managed metadata may have been maintained after v1 execution stopped.
|
|
564
|
+
// It is preserved as source bytes, never used to authorize a migration lane.
|
|
565
|
+
if (label === '.liftoff/governance/phase-graph.json') {
|
|
566
|
+
const digest = canonicalSha256(value);
|
|
567
|
+
if (digest !== historicalActivationIdentity.phaseGraphHash && digest !== currentActivationIdentity.phaseGraphHash) {
|
|
568
|
+
historyFail(label, 'is neither the registered historical nor the installed current managed graph.', 'unsupported-historical-graph');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (label === '.liftoff/governance/compatibility.json') {
|
|
572
|
+
if (isRecord(value) && value.schemaVersion === 1) {
|
|
573
|
+
validateHistoricalCompatibility(value, label);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
try {
|
|
577
|
+
const metadata = validateGovernanceCompatibilityMetadata(value);
|
|
578
|
+
for (const parts of metadata.managedCore.pathAllowlist)
|
|
579
|
+
historyPathParts(parts, `${label}.managedCore.pathAllowlist`);
|
|
580
|
+
for (const entry of metadata.managedCore.updateInventory)
|
|
581
|
+
historyPathParts(entry.pathParts, `${label}.managedCore.updateInventory.pathParts`);
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
if (!(error instanceof Error) || error.name !== 'Error')
|
|
585
|
+
throw error;
|
|
586
|
+
historyFail(label, error.message, 'invalid-managed-source-metadata');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else if (label === '.liftoff/governance/context.json') {
|
|
591
|
+
const context = historyRecord(value, label);
|
|
592
|
+
historyLiteral(context.schemaVersion, 1, `${label}.schemaVersion`);
|
|
593
|
+
const policy = historyExact(context.policy, ['profile', 'version', 'state', 'liveEnforcement'], `${label}.policy`);
|
|
594
|
+
historyLiteral(policy.profile, 'single-maintainer-gitflow', `${label}.policy.profile`);
|
|
595
|
+
historyLiteral(policy.version, '6', `${label}.policy.version`);
|
|
596
|
+
historyLiteral(policy.state, 'handoff-generated', `${label}.policy.state`);
|
|
597
|
+
historyLiteral(policy.liveEnforcement, 'not-active', `${label}.policy.liveEnforcement`);
|
|
598
|
+
const discovery = historyRecord(context.discovery, `${label}.discovery`);
|
|
599
|
+
if (Object.values(discovery).some((entry) => entry !== 'undiscovered'))
|
|
600
|
+
historyFail(label, 'context cannot assert live discovery.');
|
|
601
|
+
if (historyArray(context.commands, `${label}.commands`).length === 0)
|
|
602
|
+
historyFail(label, 'context must declare its generated commands.');
|
|
603
|
+
historyRecord(context.generatedBoundaries, `${label}.generatedBoundaries`);
|
|
604
|
+
validateEmbeddedPaths(context, label);
|
|
605
|
+
}
|
|
606
|
+
else if (label === '.liftoff/governance/credential-policy.schema.json') {
|
|
607
|
+
const schema = historyRecord(value, label);
|
|
608
|
+
historyString(schema.$schema, `${label}.$schema`);
|
|
609
|
+
const properties = historyRecord(schema.properties, `${label}.properties`);
|
|
610
|
+
const identity = historyRecord(properties.identity, `${label}.properties.identity`);
|
|
611
|
+
const identityProperties = historyExact(identity.properties, Object.keys(historicalActivationIdentity), `${label}.properties.identity.properties`);
|
|
612
|
+
const declaredIdentity = Object.fromEntries(Object.entries(identityProperties).map(([key, entry]) => {
|
|
613
|
+
const declaration = historyExact(entry, ['const'], `${label}.properties.identity.properties.${key}`);
|
|
614
|
+
return [key, declaration.const];
|
|
615
|
+
}));
|
|
616
|
+
const identityDigest = canonicalSha256(declaredIdentity);
|
|
617
|
+
if (identityDigest !== canonicalSha256(historicalActivationIdentity) && identityDigest !== canonicalSha256(currentActivationIdentity)) {
|
|
618
|
+
historyFail(label, 'credential schema must declare the exact historical or installed current activation identity.', 'invalid-managed-source-metadata');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
historyRecord(value, label);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function sourceFile(snapshot, kind) {
|
|
626
|
+
if (snapshot.content === undefined || snapshot.mode === undefined) {
|
|
627
|
+
historyFail(historyPathKey(snapshot.pathParts), 'required historical source is missing.', 'missing-historical-record');
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
kind, pathParts: [...snapshot.pathParts], content: snapshot.content,
|
|
631
|
+
digest: rawHistoryDigest(snapshot.content), mode: snapshot.mode
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
export async function historicalActiveRecordPaths(projectRoot, directory) {
|
|
635
|
+
historyRecordId(directory, 'historical active collection');
|
|
636
|
+
const parts = ['governance', directory];
|
|
637
|
+
const target = await resolveHistoryProjectPath(projectRoot, parts);
|
|
638
|
+
let entries;
|
|
639
|
+
try {
|
|
640
|
+
entries = await readdir(target, { withFileTypes: true });
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
if (errorCode(error) !== 'ENOENT')
|
|
644
|
+
throw error;
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
return entries.filter((entry) => /\.json$/iu.test(entry.name)).map((entry) => [...parts, entry.name])
|
|
648
|
+
.sort((a, b) => historyPathKey(a) < historyPathKey(b) ? -1 : 1);
|
|
649
|
+
}
|
|
650
|
+
function validateAt(file, validator) {
|
|
651
|
+
try {
|
|
652
|
+
return validator(parseHistoryJson(file.content, historyPathKey(file.pathParts)));
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (!(error instanceof ActivationHistoryError))
|
|
656
|
+
throw error;
|
|
657
|
+
return historyFail(historyPathKey(file.pathParts), error.message, error.code);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
export async function readHistoricalActivationInventory(projectRoot, options = {}) {
|
|
661
|
+
const preconditions = [];
|
|
662
|
+
const capture = async (parts) => {
|
|
663
|
+
const snapshot = await captureHistoryFile(projectRoot, parts);
|
|
664
|
+
preconditions.push(snapshot);
|
|
665
|
+
return snapshot;
|
|
666
|
+
};
|
|
667
|
+
const manifestFile = sourceFile(await capture(historicalManifestPathParts), 'manifest');
|
|
668
|
+
const stateFile = sourceFile(await capture(historicalActivationStatePathParts), 'state');
|
|
669
|
+
const manifest = validateAt(manifestFile, validateHistoricalSourceManifest);
|
|
670
|
+
const state = validateAt(stateFile, validateHistoricalActivationState);
|
|
671
|
+
if (state.activeChange !== null && state.activeChange.kind !== manifest.project.specWorkflow) {
|
|
672
|
+
historyFail(historyPathKey(stateFile.pathParts), 'active change belongs to a different spec workflow.', 'historical-spec-ownership-conflict');
|
|
673
|
+
}
|
|
674
|
+
const files = [manifestFile, stateFile];
|
|
675
|
+
for (const parts of historicalMetadataPathParts) {
|
|
676
|
+
const snapshot = await capture(parts);
|
|
677
|
+
if (snapshot.content === undefined)
|
|
678
|
+
continue;
|
|
679
|
+
const file = sourceFile(snapshot, 'metadata');
|
|
680
|
+
validateHistoricalMetadata(file);
|
|
681
|
+
files.push(file);
|
|
682
|
+
}
|
|
683
|
+
const evidence = new Map();
|
|
684
|
+
const plans = [];
|
|
685
|
+
const approvals = new Map();
|
|
686
|
+
for (const [directory, kind] of [['evidence', 'evidence'], ['plans', 'plan'], ['approvals', 'approval']]) {
|
|
687
|
+
for (const parts of await historicalActiveRecordPaths(projectRoot, directory)) {
|
|
688
|
+
const file = sourceFile(await capture(parts), kind);
|
|
689
|
+
if (!parts[2].endsWith('.json'))
|
|
690
|
+
historyFail(historyPathKey(parts), 'active record extension is not the registered lowercase .json layout.');
|
|
691
|
+
if (kind === 'evidence') {
|
|
692
|
+
const record = validateAt(file, (value) => validateHistoricalEvidenceRecord(value, parts[2].slice(0, -5)));
|
|
693
|
+
if (record.header.repositoryId !== state.repository.id)
|
|
694
|
+
historyFail(historyPathKey(parts), 'historical evidence belongs to another repository.', 'invalid-historical-reference');
|
|
695
|
+
if (evidence.has(record.evidenceId))
|
|
696
|
+
historyFail(historyPathKey(parts), 'duplicates an evidence identity.');
|
|
697
|
+
evidence.set(record.evidenceId, { file, record });
|
|
698
|
+
}
|
|
699
|
+
else if (kind === 'plan') {
|
|
700
|
+
plans.push({ file, record: validateAt(file, validateHistoricalSavedTransitionPlan) });
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const record = validateAt(file, validateHistoricalApprovalEnvelope);
|
|
704
|
+
if (approvals.has(record.id))
|
|
705
|
+
historyFail(historyPathKey(parts), 'duplicates an approval identity.');
|
|
706
|
+
approvals.set(record.id, { file, record });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
for (const directory of ['supersessions', 'reconciliation']) {
|
|
711
|
+
const records = await historicalActiveRecordPaths(projectRoot, directory);
|
|
712
|
+
if (records.length > 0)
|
|
713
|
+
historyFail(historyPathKey(records[0]), 'active historical auxiliary proof has no registered successor retirement contract; preserve it for explicit reconciliation.', 'unsupported-active-record');
|
|
714
|
+
}
|
|
715
|
+
for (const parts of [['governance', 'credentials', 'preflight-policy.json'], ['governance', 'activation-baseline.json']]) {
|
|
716
|
+
if ((await capture(parts)).content !== undefined) {
|
|
717
|
+
historyFail(historyPathKey(parts), 'active auxiliary proof has no registered v1 retirement contract; it cannot be silently carried forward.', 'unsupported-active-record');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const selected = new Set();
|
|
721
|
+
const select = (file) => selected.add(historyPathKey(file.pathParts));
|
|
722
|
+
const requireApproval = (id, expectedPhase, expectedHash) => {
|
|
723
|
+
const found = approvals.get(id);
|
|
724
|
+
if (!found)
|
|
725
|
+
historyFail(`governance/approvals/${id}`, 'referenced historical approval is missing.', 'missing-historical-record');
|
|
726
|
+
if (found.record.phaseId !== expectedPhase || expectedHash !== undefined && historicalApprovalEnvelopeHash(found.record) !== expectedHash) {
|
|
727
|
+
historyFail(historyPathKey(found.file.pathParts), 'approval phase or scope hash contradicts its reference.', 'invalid-historical-reference');
|
|
728
|
+
}
|
|
729
|
+
select(found.file);
|
|
730
|
+
};
|
|
731
|
+
const selectPlan = (entry) => {
|
|
732
|
+
select(entry.file);
|
|
733
|
+
if (entry.record.approval.envelopeId !== null) {
|
|
734
|
+
if (entry.record.approval.envelopeHash === null)
|
|
735
|
+
historyFail(historyPathKey(entry.file.pathParts), 'approval reference has no hash.');
|
|
736
|
+
requireApproval(entry.record.approval.envelopeId, entry.record.phaseId, entry.record.approval.envelopeHash);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
const selectEvidence = (entry) => {
|
|
740
|
+
select(entry.file);
|
|
741
|
+
const { header, payload, evidenceId } = entry.record;
|
|
742
|
+
const matching = plans.filter((candidate) => candidate.record.phaseId === header.phaseId && candidate.record.transitionDigest === header.transition.transitionDigest &&
|
|
743
|
+
candidate.record.baselineDigest === header.baselineSha && candidate.record.inputDigest === header.inputDigest);
|
|
744
|
+
const planDigest = isRecord(payload) && Object.hasOwn(payload, 'planDigest')
|
|
745
|
+
? historyDigest(payload.planDigest, `${evidenceId}.payload.planDigest`) : undefined;
|
|
746
|
+
const savedPlanDigest = isRecord(payload) && Object.hasOwn(payload, 'savedPlanDigest')
|
|
747
|
+
? historyDigest(payload.savedPlanDigest, `${evidenceId}.payload.savedPlanDigest`) : undefined;
|
|
748
|
+
const linked = matching.filter((candidate) => (planDigest === undefined || candidate.record.planDigest === planDigest) &&
|
|
749
|
+
(savedPlanDigest === undefined || canonicalSha256(candidate.record) === savedPlanDigest));
|
|
750
|
+
if (linked.length === 0 && (planDigest !== undefined || savedPlanDigest !== undefined || header.producer === 'liftoff-governance-transition-engine')) {
|
|
751
|
+
historyFail(historyPathKey(entry.file.pathParts), 'required reviewed historical transition plan is missing or inconsistent.', 'missing-historical-record');
|
|
752
|
+
}
|
|
753
|
+
linked.forEach(selectPlan);
|
|
754
|
+
};
|
|
755
|
+
for (const id of historicalPhaseIds) {
|
|
756
|
+
const phase = state.phases[id];
|
|
757
|
+
if (['verified', 'inapplicable', 'retained', 'disposed'].includes(phase.state) && phase.evidence.length === 0 ||
|
|
758
|
+
phase.state === 'approved' && phase.approvals.length === 0) {
|
|
759
|
+
historyFail(`governance/activation-state.json#phases.${id}`, 'terminal historical state has no required record references.', 'missing-historical-record');
|
|
760
|
+
}
|
|
761
|
+
for (const ref of phase.evidence) {
|
|
762
|
+
const found = evidence.get(ref.evidenceId);
|
|
763
|
+
if (!found)
|
|
764
|
+
historyFail(`governance/evidence/${ref.evidenceId}.json`, 'referenced historical evidence is missing.', 'missing-historical-record');
|
|
765
|
+
const header = found.record.header;
|
|
766
|
+
if (header.phaseId !== id || header.result !== ref.result || canonicalSha256(header) !== ref.headerDigest) {
|
|
767
|
+
historyFail(historyPathKey(found.file.pathParts), 'header digest, phase or result contradicts its state reference.', 'invalid-historical-reference');
|
|
768
|
+
}
|
|
769
|
+
selectEvidence(found);
|
|
770
|
+
}
|
|
771
|
+
for (const approvalId of phase.approvals)
|
|
772
|
+
requireApproval(approvalId, id);
|
|
773
|
+
}
|
|
774
|
+
if (state.bootstrapState !== undefined) {
|
|
775
|
+
const retained = evidence.get(state.bootstrapState.remoteImportEvidenceId);
|
|
776
|
+
if (!retained || retained.record.header.phaseId !== 'remote-import-verified' ||
|
|
777
|
+
canonicalSha256(retained.record.header) !== state.bootstrapState.remoteImportEvidenceDigest) {
|
|
778
|
+
historyFail('governance/activation-state.json#bootstrapState', 'remote import evidence reference is missing or inconsistent.', 'invalid-historical-reference');
|
|
779
|
+
}
|
|
780
|
+
selectEvidence(retained);
|
|
781
|
+
if (state.bootstrapState.deletionEvidenceId !== undefined) {
|
|
782
|
+
const deletion = evidence.get(state.bootstrapState.deletionEvidenceId);
|
|
783
|
+
if (!deletion || deletion.record.header.phaseId !== 'bootstrap-state-disposed') {
|
|
784
|
+
historyFail('governance/activation-state.json#bootstrapState.deletionEvidenceId', 'referenced disposal evidence is missing or names another phase.', 'missing-historical-record');
|
|
785
|
+
}
|
|
786
|
+
selectEvidence(deletion);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const allRecords = [...evidence.values(), ...plans, ...approvals.values()];
|
|
790
|
+
const requested = new Set();
|
|
791
|
+
for (const raw of options.reviewedUnreferencedPathParts ?? []) {
|
|
792
|
+
const key = historyPathKey(historyPathParts(raw, 'reviewed unreferenced path'));
|
|
793
|
+
if (requested.has(key))
|
|
794
|
+
historyFail(key, 'duplicates a reviewed inventory entry.');
|
|
795
|
+
requested.add(key);
|
|
796
|
+
const found = allRecords.find((entry) => historyPathKey(entry.file.pathParts) === key);
|
|
797
|
+
if (!found)
|
|
798
|
+
historyFail(key, 'is not a recognized existing historical record.', 'unregistered-history-source');
|
|
799
|
+
select(found.file);
|
|
800
|
+
const plan = plans.find((entry) => entry.file === found.file);
|
|
801
|
+
if (plan)
|
|
802
|
+
selectPlan(plan);
|
|
803
|
+
const proof = [...evidence.values()].find((entry) => entry.file === found.file);
|
|
804
|
+
if (proof)
|
|
805
|
+
selectEvidence(proof);
|
|
806
|
+
}
|
|
807
|
+
const unreviewedRecords = [];
|
|
808
|
+
for (const entry of allRecords) {
|
|
809
|
+
if (selected.has(historyPathKey(entry.file.pathParts)))
|
|
810
|
+
files.push(entry.file);
|
|
811
|
+
else
|
|
812
|
+
unreviewedRecords.push(entry.file);
|
|
813
|
+
}
|
|
814
|
+
files.sort((a, b) => historyPathKey(a.pathParts) < historyPathKey(b.pathParts) ? -1 : 1);
|
|
815
|
+
return { manifest, state, files, unreviewedRecords, preconditions };
|
|
816
|
+
}
|
|
817
|
+
//# sourceMappingURL=historical-state.js.map
|