@tiinex/core 0.12.0 → 0.14.0
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/package.json +6 -5
- package/src/audit/audit.run.js +1 -1
- package/src/public/index.js +18 -0
- package/src/public/node.js +1 -0
- package/src/schemas/creation.contracts.js +1 -1
- package/src/schemas/creation.schemaReferences.js +28 -8
- package/src/schemas/schema.reference.js +30 -4
- package/src/schemas/tiinex.root.v1.schema.json +2 -1
- package/src/tooling/portable/adapters/cli/cli.command-input.js +5 -0
- package/src/tooling/portable/adapters/cli/cli.common-output.js +23 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +52 -11
- package/src/tooling/portable/adapters/cli/cli.help.js +15 -2
- package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
- package/src/tooling/portable/adapters/cli/cli.source-frontier-comparison.js +6 -6
- package/src/tooling/portable/adapters/cli/cli.source-frontier-reconciliation.js +18 -0
- package/src/tooling/portable/adapters/node/bootstrapCarrier.manufacture.js +18 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.enumeration.js +27 -8
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +17 -0
- package/src/tooling/portable/adapters/node/sourceFrontierComparison.js +2 -1
- package/src/tooling/portable/adapters/node/sourceFrontierReconciliationProof.js +23 -0
- package/src/tooling/portable/adapters/node/workspaceCarrier.manufacture.js +14 -3
- package/src/tooling/portable/audit/audit.capability.js +2 -1
- package/src/tooling/portable/bootstrap/tiinex.llm.bootstrap.md +2 -2
- package/src/tooling/portable/comparison/sourceFrontierComparison.js +54 -4
- package/src/tooling/portable/comparison/sourceFrontierReconciliationProof.js +569 -0
- package/src/tooling/portable/draft/draft.create.js +3 -0
- package/src/tooling/portable/draft/draft.operations.js +1 -1
- package/src/tooling/portable/editor/editor.assistance.js +2 -25
- package/src/tooling/portable/handoff/bootstrapCarrier.manufacture.js +56 -0
- package/src/tooling/portable/handoff/contracts/tiinex.handoff.package.v1.schema.md +340 -77
- package/src/tooling/portable/handoff/manufacture.js +17 -3
- package/src/tooling/portable/handoff/pointerEntrypoint.js +4 -1
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.build.js +73 -32
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.closure.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.inspect.js +9 -1
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.materials.js +28 -6
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.shared.js +8 -0
- package/src/tooling/portable/handoff/recipientV2.coldProjection.js +11 -6
- package/src/tooling/portable/handoff/recipientV2.entryContract.js +12 -0
- package/src/tooling/portable/handoff/recipientV2.humanOutput.js +48 -6
- package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +55 -43
- package/src/tooling/portable/handoff/recipientV2.packageV1.contract.js +57 -31
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +63 -10
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +55 -16
- package/src/tooling/portable/handoff/recipientV2.packageV1.workspaceProjection.js +14 -1
- package/src/tooling/portable/handoff/recipientV2.topology.js +3 -4
- package/src/tooling/portable/handoff/recipientV2.topology.materials.js +14 -0
- package/src/tooling/portable/handoff/schemaReferencePreflight.js +26 -0
- package/src/tooling/portable/handoff/transportEnvelopeV1.js +5 -0
- package/src/tooling/portable/handoff/workspaceCarrier.manufacture.js +42 -13
- package/src/tooling/portable/index.js +1 -0
- package/src/tooling/portable/operation.catalog.js +8 -0
- package/src/tooling/portable/source/sourceEligibility.js +108 -0
- package/src/validation/validateArtifact.js +62 -6
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { sha256Hex, utf8Bytes } from '../../../export/package.bytes.js';
|
|
2
|
+
import { portableFinding, summarizePortableFindings } from '../findings.js';
|
|
3
|
+
import {
|
|
4
|
+
PORTABLE_SOURCE_FRONTIER_SCHEMA_ID,
|
|
5
|
+
createPortableSourceFrontier,
|
|
6
|
+
createPortableWorkspaceSnapshot,
|
|
7
|
+
reconcilePortableSourceFrontiers
|
|
8
|
+
} from './sourceFrontierComparison.js';
|
|
9
|
+
|
|
10
|
+
export const PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_SCHEMA_ID = 'tiinex.portable.source-frontier-reconciliation-proof.v1';
|
|
11
|
+
export const PORTABLE_SOURCE_FRONTIER_RECONCILIATION_SUMMARY_SCHEMA_ID = 'tiinex.portable.source-frontier-reconciliation-proof-summary.v1';
|
|
12
|
+
export const PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_BASIS_SCHEMA_ID = 'tiinex.portable.source-frontier-reconciliation-proof-basis.v1';
|
|
13
|
+
|
|
14
|
+
const QUALIFIED = 'qualified';
|
|
15
|
+
const DISPOSITION_ACTIONS = new Set(['incoming', 'current', 'base', 'reconciled', 'delete']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prove one candidate reconciled source frontier against qualified base/incoming/current
|
|
19
|
+
* byte identity. Non-overlap preservation is mechanical. Conflict/deletion paths require an
|
|
20
|
+
* explicit caller disposition and the receipt never treats that disposition as semantic truth.
|
|
21
|
+
*/
|
|
22
|
+
export function provePortableSourceReconciliation(input = {}) {
|
|
23
|
+
const base = ensureFrontier(input.base, 'base');
|
|
24
|
+
const incoming = ensureFrontier(input.incoming, 'incoming');
|
|
25
|
+
const current = ensureFrontier(input.current, 'current');
|
|
26
|
+
const reconciled = ensureFrontier(input.reconciled, 'reconciled');
|
|
27
|
+
const comparison = reconcilePortableSourceFrontiers({ base, incoming, current });
|
|
28
|
+
const findings = [...(comparison.findings || []), ...(reconciled.findings || [])];
|
|
29
|
+
const dispositions = normalizeDispositions(input.dispositions, findings);
|
|
30
|
+
const dispositionByKey = new Map(dispositions.map((item) => [dispositionKey(item.workspaceId, item.path), item]));
|
|
31
|
+
const usedDispositionKeys = new Set();
|
|
32
|
+
|
|
33
|
+
if (!input.reconciled) {
|
|
34
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.reconciled-required', 'Manufacture reconciliation proof requires an explicit candidate reconciled frontier.'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const baseById = workspaceMap(base);
|
|
38
|
+
const incomingById = workspaceMap(incoming);
|
|
39
|
+
const currentById = workspaceMap(current);
|
|
40
|
+
const reconciledById = workspaceMap(reconciled);
|
|
41
|
+
const sourceIds = [...new Set([...baseById.keys(), ...incomingById.keys(), ...currentById.keys()])].sort();
|
|
42
|
+
for (const workspaceId of reconciledById.keys()) {
|
|
43
|
+
if (sourceIds.includes(workspaceId)) continue;
|
|
44
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.workspace-unexpected', 'Candidate reconciled frontier contains a Workspace that is not present in the qualified base/incoming/current source union.', { workspaceId }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const workspaces = sourceIds.map((workspaceId) => reconcileWorkspaceForManufacture({
|
|
48
|
+
workspaceId,
|
|
49
|
+
base: baseById.get(workspaceId),
|
|
50
|
+
incoming: incomingById.get(workspaceId),
|
|
51
|
+
current: currentById.get(workspaceId),
|
|
52
|
+
reconciled: reconciledById.get(workspaceId),
|
|
53
|
+
dispositionByKey,
|
|
54
|
+
usedDispositionKeys,
|
|
55
|
+
findings
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
for (const disposition of dispositions) {
|
|
59
|
+
const key = dispositionKey(disposition.workspaceId, disposition.path);
|
|
60
|
+
if (usedDispositionKeys.has(key)) continue;
|
|
61
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-orphan', 'Explicit disposition does not match a conflict/deletion candidate in the qualified source union.', { workspaceId: disposition.workspaceId, path: disposition.path }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalizedFindings = Object.freeze(dedupeFindings(findings));
|
|
65
|
+
const counts = aggregateProofCounts(workspaces);
|
|
66
|
+
const qualificationFailure = !input.reconciled
|
|
67
|
+
|| base.state !== QUALIFIED
|
|
68
|
+
|| incoming.state !== QUALIFIED
|
|
69
|
+
|| current.state !== QUALIFIED
|
|
70
|
+
|| reconciled.state !== QUALIFIED;
|
|
71
|
+
const unresolved = counts.unresolvedDisposition > 0;
|
|
72
|
+
const mismatch = counts.reconciledMismatch > 0 || normalizedFindings.some((item) => item.code === 'portable.source-frontier.reconciliation.reconciled-path-unexpected');
|
|
73
|
+
const otherErrors = normalizedFindings.some((item) => item.severity === 'error');
|
|
74
|
+
const state = qualificationFailure
|
|
75
|
+
? 'qualification-error'
|
|
76
|
+
: unresolved
|
|
77
|
+
? 'disposition-required'
|
|
78
|
+
: mismatch || otherErrors
|
|
79
|
+
? 'reconciled-frontier-mismatch'
|
|
80
|
+
: 'manufacture-ready';
|
|
81
|
+
const status = state === 'manufacture-ready' ? 'ready' : 'blocked';
|
|
82
|
+
|
|
83
|
+
const preservation = deepFreeze({
|
|
84
|
+
incomingOnly: Object.freeze({ accepted: counts.incomingOnly, preserved: counts.incomingOnlyPreserved, failed: Math.max(0, counts.incomingOnly - counts.incomingOnlyPreserved) }),
|
|
85
|
+
currentOnly: Object.freeze({ accepted: counts.currentOnly, preserved: counts.currentOnlyPreserved, failed: Math.max(0, counts.currentOnly - counts.currentOnlyPreserved) })
|
|
86
|
+
});
|
|
87
|
+
const proofBasis = deepFreeze({
|
|
88
|
+
schema: PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_BASIS_SCHEMA_ID,
|
|
89
|
+
state,
|
|
90
|
+
inputs: Object.freeze({
|
|
91
|
+
base: frontierBindingReceipt(base),
|
|
92
|
+
incoming: frontierBindingReceipt(incoming),
|
|
93
|
+
current: frontierBindingReceipt(current),
|
|
94
|
+
reconciled: frontierBindingReceipt(reconciled)
|
|
95
|
+
}),
|
|
96
|
+
dispositions: Object.freeze(dispositions.map(dispositionBindingReceipt)),
|
|
97
|
+
workspaces: Object.freeze(workspaces.map(workspaceBindingReceipt)),
|
|
98
|
+
preservation
|
|
99
|
+
});
|
|
100
|
+
const proofFingerprint = sha256Hex(utf8Bytes(stableJson(proofBasis)));
|
|
101
|
+
const manufactureBinding = deepFreeze({
|
|
102
|
+
state: status === 'ready' ? 'qualified' : 'blocked',
|
|
103
|
+
proofFingerprint,
|
|
104
|
+
workspaces: Object.freeze(workspaces.filter((workspace) => workspace.reconciled?.snapshot).map((workspace) => Object.freeze({
|
|
105
|
+
workspaceId: workspace.workspaceId,
|
|
106
|
+
snapshot: Object.freeze({
|
|
107
|
+
entryCount: workspace.reconciled.snapshot.entryCount,
|
|
108
|
+
totalBytes: workspace.reconciled.snapshot.totalBytes,
|
|
109
|
+
fingerprint: workspace.reconciled.snapshot.fingerprint,
|
|
110
|
+
fingerprintMethod: workspace.reconciled.snapshot.fingerprintMethod
|
|
111
|
+
})
|
|
112
|
+
}))),
|
|
113
|
+
boundary: 'Binds this proof to the exact candidate reconciled Workspace snapshot fingerprints. Manufacture must re-enumerate source and match these fingerprints before carriage.'
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return deepFreeze({
|
|
117
|
+
schema: PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_SCHEMA_ID,
|
|
118
|
+
status,
|
|
119
|
+
state,
|
|
120
|
+
comparison: Object.freeze({ schema: comparison.schema, state: comparison.state, counts: comparison.counts }),
|
|
121
|
+
inputs: Object.freeze({
|
|
122
|
+
base: frontierBindingReceipt(base),
|
|
123
|
+
incoming: frontierBindingReceipt(incoming),
|
|
124
|
+
current: frontierBindingReceipt(current),
|
|
125
|
+
reconciled: frontierBindingReceipt(reconciled)
|
|
126
|
+
}),
|
|
127
|
+
workspaces: Object.freeze(workspaces),
|
|
128
|
+
counts,
|
|
129
|
+
preservation,
|
|
130
|
+
dispositions: Object.freeze(dispositions),
|
|
131
|
+
proofBasis,
|
|
132
|
+
proofFingerprint,
|
|
133
|
+
manufactureBinding,
|
|
134
|
+
findings: normalizedFindings,
|
|
135
|
+
findingSummary: summarizePortableFindings(normalizedFindings),
|
|
136
|
+
operationBoundary: Object.freeze({
|
|
137
|
+
sourceMutation: false,
|
|
138
|
+
remoteFetch: false,
|
|
139
|
+
remoteMutation: false,
|
|
140
|
+
automaticMerge: false,
|
|
141
|
+
semanticDisposition: false,
|
|
142
|
+
acceptanceInference: false,
|
|
143
|
+
manufactureGate: true
|
|
144
|
+
}),
|
|
145
|
+
boundary: 'Mechanical base/incoming/current reconciliation proof only. Non-overlap byte preservation is verified; conflict/deletion candidates require explicit caller disposition. A disposition records a decision but does not prove semantic correctness, authority, acceptance, or intentional deletion.'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Compact projection; full proof is required when a manufacture command consumes the receipt. */
|
|
150
|
+
export function projectPortableSourceReconciliationProofSummary(result = {}, options = {}) {
|
|
151
|
+
const maxPaths = positiveInteger(options.maxPaths, 20);
|
|
152
|
+
return deepFreeze({
|
|
153
|
+
schema: PORTABLE_SOURCE_FRONTIER_RECONCILIATION_SUMMARY_SCHEMA_ID,
|
|
154
|
+
resultSchema: String(result.schema || ''),
|
|
155
|
+
status: String(result.status || ''),
|
|
156
|
+
state: String(result.state || ''),
|
|
157
|
+
counts: result.counts ? Object.freeze({ ...result.counts }) : null,
|
|
158
|
+
preservation: result.preservation ? deepFreeze(result.preservation) : null,
|
|
159
|
+
workspaces: Object.freeze((result.workspaces || []).map((workspace) => Object.freeze({
|
|
160
|
+
workspaceId: String(workspace.workspaceId || ''),
|
|
161
|
+
state: String(workspace.state || ''),
|
|
162
|
+
counts: workspace.counts ? Object.freeze({ ...workspace.counts }) : null,
|
|
163
|
+
paths: Object.freeze((workspace.paths || []).slice(0, maxPaths).map((item) => Object.freeze({
|
|
164
|
+
path: item.path,
|
|
165
|
+
classification: item.classification,
|
|
166
|
+
deletionCandidate: Boolean(item.deletionCandidate),
|
|
167
|
+
conflictCandidate: Boolean(item.conflictCandidate),
|
|
168
|
+
dispositionRequired: Boolean(item.dispositionRequired),
|
|
169
|
+
dispositionState: item.dispositionState,
|
|
170
|
+
reconciledMatch: item.reconciledMatch
|
|
171
|
+
}))),
|
|
172
|
+
pathsOmitted: Math.max(0, (workspace.paths || []).length - maxPaths),
|
|
173
|
+
unexpectedReconciledPaths: Object.freeze([...(workspace.unexpectedReconciledPaths || [])].slice(0, maxPaths)),
|
|
174
|
+
unexpectedReconciledPathsOmitted: Math.max(0, (workspace.unexpectedReconciledPaths || []).length - maxPaths)
|
|
175
|
+
}))),
|
|
176
|
+
proofFingerprint: String(result.proofFingerprint || ''),
|
|
177
|
+
manufactureBinding: result.manufactureBinding ? deepFreeze(result.manufactureBinding) : null,
|
|
178
|
+
findingSummary: result.findingSummary || summarizePortableFindings(result.findings || []),
|
|
179
|
+
actionableFindings: Object.freeze((result.findings || []).filter((item) => item.severity === 'error' || item.severity === 'warning').slice(0, 20).map((item) => Object.freeze({ ...item }))),
|
|
180
|
+
fullReceiptRequiredForManufacture: true,
|
|
181
|
+
boundary: 'Compact reconciliation proof projection. Use --full when persisting a proof for manufacture; semantic merge correctness remains outside Tooling.'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Requalify a full reconciliation proof against the exact Workspace bytes about to be manufactured.
|
|
187
|
+
* This catches stale/missing proof and candidate-source drift without inferring semantic acceptance.
|
|
188
|
+
*/
|
|
189
|
+
export function qualifyPortableSourceReconciliationProofForManufacture(input = {}) {
|
|
190
|
+
const requireProof = input.requireProof === true;
|
|
191
|
+
const supplied = input.proof || null;
|
|
192
|
+
const findings = [];
|
|
193
|
+
if (!supplied || (typeof supplied === 'object' && !Object.keys(supplied).length)) {
|
|
194
|
+
if (!requireProof) return deepFreeze({ state: 'not-required', findings: Object.freeze([]), findingSummary: summarizePortableFindings([]), boundary: 'No reconciliation proof was requested for this manufacture.' });
|
|
195
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.required', 'This manufacture requires a qualified source-reconciliation proof receipt, but none was supplied.'));
|
|
196
|
+
return blockedManufactureQualification(findings);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const proof = unwrapProof(supplied);
|
|
200
|
+
if (!proof || proof.schema !== PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_SCHEMA_ID) {
|
|
201
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.schema-invalid', 'Supplied reconciliation proof is not a full supported source-reconciliation proof receipt.'));
|
|
202
|
+
return blockedManufactureQualification(findings);
|
|
203
|
+
}
|
|
204
|
+
if (proof.status !== 'ready' || proof.state !== 'manufacture-ready' || proof.manufactureBinding?.state !== 'qualified') {
|
|
205
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.not-ready', 'Supplied reconciliation proof is not manufacture-ready.', { proofState: String(proof.state || ''), proofStatus: String(proof.status || '') }));
|
|
206
|
+
}
|
|
207
|
+
if (!proof.proofBasis || proof.proofBasis.schema !== PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_BASIS_SCHEMA_ID) {
|
|
208
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.full-receipt-required', 'Manufacture requires the full reconciliation proof receipt including its deterministic proof basis; compact projections are insufficient.'));
|
|
209
|
+
} else {
|
|
210
|
+
const expectedFingerprint = sha256Hex(utf8Bytes(stableJson(proof.proofBasis)));
|
|
211
|
+
if (!proof.proofFingerprint || proof.proofFingerprint !== expectedFingerprint || proof.manufactureBinding?.proofFingerprint !== expectedFingerprint) {
|
|
212
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.fingerprint-mismatch', 'Reconciliation proof fingerprint does not match its deterministic proof basis.'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const materializationById = new Map((input.workspaceMaterializations || []).map((materialization) => [String(materialization?.id || materialization?.workspaceId || ''), materialization]).filter(([id]) => id));
|
|
217
|
+
const bindingById = new Map((proof.manufactureBinding?.workspaces || []).map((binding) => [String(binding?.workspaceId || ''), binding]).filter(([id]) => id));
|
|
218
|
+
const requiredWorkspaceIds = [...new Set((input.requiredWorkspaceIds || []).map(String).filter(Boolean))].sort();
|
|
219
|
+
for (const workspaceId of requiredWorkspaceIds) {
|
|
220
|
+
if (!bindingById.has(workspaceId)) findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.workspace-not-covered', 'Required manufactured Workspace is not covered by the reconciliation proof.', { workspaceId }));
|
|
221
|
+
}
|
|
222
|
+
for (const [workspaceId, binding] of bindingById) {
|
|
223
|
+
const materialization = materializationById.get(workspaceId);
|
|
224
|
+
if (!materialization) {
|
|
225
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.workspace-missing', 'Reconciliation proof covers a Workspace that is not present in current manufacture input.', { workspaceId }));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const actual = createPortableWorkspaceSnapshot(materialization.includedEntries || materialization.entries || [], { source: 'handoff-manufacture-requalification' });
|
|
229
|
+
const expected = binding.snapshot || {};
|
|
230
|
+
if (actual.state !== QUALIFIED || actual.fingerprint !== expected.fingerprint || actual.entryCount !== Number(expected.entryCount || 0) || actual.totalBytes !== Number(expected.totalBytes || 0)) {
|
|
231
|
+
findings.push(portableFinding('error', 'portable.handoff-manufacture.reconciliation-proof.source-drift', 'Workspace bytes selected for manufacture do not match the candidate reconciled source fingerprint proven by the supplied receipt.', { workspaceId, expectedFingerprint: String(expected.fingerprint || ''), actualFingerprint: String(actual.fingerprint || '') }));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const normalizedFindings = Object.freeze(dedupeFindings(findings));
|
|
236
|
+
if (normalizedFindings.some((item) => item.severity === 'error')) return blockedManufactureQualification(normalizedFindings, proof);
|
|
237
|
+
return deepFreeze({
|
|
238
|
+
state: 'qualified',
|
|
239
|
+
proofFingerprint: String(proof.proofFingerprint || ''),
|
|
240
|
+
coveredWorkspaceIds: Object.freeze([...bindingById.keys()].sort()),
|
|
241
|
+
requiredWorkspaceIds: Object.freeze(requiredWorkspaceIds),
|
|
242
|
+
findings: normalizedFindings,
|
|
243
|
+
findingSummary: summarizePortableFindings(normalizedFindings),
|
|
244
|
+
boundary: 'Qualified only for exact source-byte identity at manufacture time. Semantic merge correctness, acceptance, and authority remain external.'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function reconcileWorkspaceForManufacture({ workspaceId, base, incoming, current, reconciled, dispositionByKey, usedDispositionKeys, findings }) {
|
|
249
|
+
const sourceSides = { base, incoming, current };
|
|
250
|
+
const missingSides = Object.entries(sourceSides).filter(([, value]) => !value).map(([key]) => key);
|
|
251
|
+
if (missingSides.length) {
|
|
252
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.workspace-source-incomplete', 'Reconciliation proof requires the Workspace to be present in base, incoming, and current frontiers.', { workspaceId, missing: missingSides.join(',') }));
|
|
253
|
+
return unavailableWorkspace(workspaceId, sourceSides, reconciled, missingSides);
|
|
254
|
+
}
|
|
255
|
+
const nonqualified = Object.entries({ ...sourceSides, reconciled }).filter(([, value]) => !value || value.state !== QUALIFIED).map(([key, value]) => `${key}:${String(value?.state || 'missing')}`);
|
|
256
|
+
if (nonqualified.length) {
|
|
257
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.workspace-unqualified', 'Reconciliation proof requires qualified, non-opaque source snapshots for base, incoming, current, and reconciled Workspace states.', { workspaceId, states: nonqualified.join(',') }));
|
|
258
|
+
return unavailableWorkspace(workspaceId, sourceSides, reconciled, nonqualified, 'qualification-error');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const baseMap = entryMap(base.snapshot);
|
|
262
|
+
const incomingMap = entryMap(incoming.snapshot);
|
|
263
|
+
const currentMap = entryMap(current.snapshot);
|
|
264
|
+
const reconciledMap = entryMap(reconciled.snapshot);
|
|
265
|
+
const sourcePaths = [...new Set([...baseMap.keys(), ...incomingMap.keys(), ...currentMap.keys()])].sort();
|
|
266
|
+
const sourcePathSet = new Set(sourcePaths);
|
|
267
|
+
const unexpectedReconciledPaths = [...reconciledMap.keys()].filter((path) => !sourcePathSet.has(path)).sort();
|
|
268
|
+
for (const path of unexpectedReconciledPaths) {
|
|
269
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.reconciled-path-unexpected', 'Candidate reconciled frontier contains source bytes not justified by any base/incoming/current path.', { workspaceId, path }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const paths = [];
|
|
273
|
+
const expectedEntries = [];
|
|
274
|
+
const counts = {
|
|
275
|
+
exact: 0,
|
|
276
|
+
incomingOnly: 0,
|
|
277
|
+
currentOnly: 0,
|
|
278
|
+
sameResultConcurrent: 0,
|
|
279
|
+
deletionCandidate: 0,
|
|
280
|
+
conflictingOverlap: 0,
|
|
281
|
+
dispositionRequired: 0,
|
|
282
|
+
resolvedDisposition: 0,
|
|
283
|
+
unresolvedDisposition: 0,
|
|
284
|
+
reconciledMatch: 0,
|
|
285
|
+
reconciledMismatch: 0,
|
|
286
|
+
incomingOnlyPreserved: 0,
|
|
287
|
+
currentOnlyPreserved: 0,
|
|
288
|
+
totalPaths: sourcePaths.length
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
for (const path of sourcePaths) {
|
|
292
|
+
const baseEntry = baseMap.get(path) || null;
|
|
293
|
+
const incomingEntry = incomingMap.get(path) || null;
|
|
294
|
+
const currentEntry = currentMap.get(path) || null;
|
|
295
|
+
const reconciledEntry = reconciledMap.get(path) || null;
|
|
296
|
+
const classified = classifyPath(baseEntry, incomingEntry, currentEntry);
|
|
297
|
+
counts[classified.countKey] += 1;
|
|
298
|
+
if (classified.deletionCandidate && classified.countKey !== 'deletionCandidate') counts.deletionCandidate += 1;
|
|
299
|
+
if (classified.conflictCandidate && classified.countKey !== 'conflictingOverlap') counts.conflictingOverlap += 1;
|
|
300
|
+
|
|
301
|
+
const key = dispositionKey(workspaceId, path);
|
|
302
|
+
const disposition = dispositionByKey.get(key) || null;
|
|
303
|
+
let selected = null;
|
|
304
|
+
let selectionState = 'automatic';
|
|
305
|
+
let dispositionState = 'not-required';
|
|
306
|
+
let selectionSource = '';
|
|
307
|
+
let selectionQualified = true;
|
|
308
|
+
|
|
309
|
+
if (classified.dispositionRequired) {
|
|
310
|
+
counts.dispositionRequired += 1;
|
|
311
|
+
if (!disposition) {
|
|
312
|
+
counts.unresolvedDisposition += 1;
|
|
313
|
+
selectionState = 'unresolved';
|
|
314
|
+
dispositionState = 'required';
|
|
315
|
+
selectionQualified = false;
|
|
316
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-required', 'Conflict/deletion candidate requires an explicit disposition before manufacture can be ready.', { workspaceId, path, classification: classified.classification }));
|
|
317
|
+
} else {
|
|
318
|
+
usedDispositionKeys.add(key);
|
|
319
|
+
const resolution = resolveDisposition(disposition, { baseEntry, incomingEntry, currentEntry, reconciledEntry });
|
|
320
|
+
selectionQualified = resolution.qualified;
|
|
321
|
+
selected = resolution.entry;
|
|
322
|
+
selectionSource = resolution.source;
|
|
323
|
+
selectionState = resolution.qualified ? 'explicit' : 'invalid';
|
|
324
|
+
dispositionState = resolution.qualified ? 'resolved' : 'invalid';
|
|
325
|
+
if (resolution.qualified) counts.resolvedDisposition += 1;
|
|
326
|
+
else findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-invalid-for-path', resolution.message, { workspaceId, path, action: disposition.action }));
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
if (disposition) {
|
|
330
|
+
usedDispositionKeys.add(key);
|
|
331
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-unnecessary', 'Disposition was supplied for a path that is not a conflict/deletion candidate; non-overlap preservation is deterministic and must not be overridden.', { workspaceId, path, action: disposition.action }));
|
|
332
|
+
selectionQualified = false;
|
|
333
|
+
}
|
|
334
|
+
selected = automaticSelection(classified.classification, { baseEntry, incomingEntry, currentEntry });
|
|
335
|
+
selectionSource = automaticSelectionSource(classified.classification);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let reconciledMatch = 'unverified';
|
|
339
|
+
if (selectionQualified && (!classified.dispositionRequired || disposition)) {
|
|
340
|
+
const matched = sameOptionalEntry(selected, reconciledEntry);
|
|
341
|
+
reconciledMatch = matched ? 'matched' : 'mismatch';
|
|
342
|
+
if (matched) counts.reconciledMatch += 1;
|
|
343
|
+
else {
|
|
344
|
+
counts.reconciledMismatch += 1;
|
|
345
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.reconciled-path-mismatch', 'Candidate reconciled source does not match the mechanically selected source identity for this path.', { workspaceId, path, classification: classified.classification, selectionSource }));
|
|
346
|
+
}
|
|
347
|
+
if (selected) expectedEntries.push({ path, bytes: selected.bytes, sha256: selected.sha256 });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (classified.classification === 'incoming-only' && reconciledMatch === 'matched') counts.incomingOnlyPreserved += 1;
|
|
351
|
+
if (classified.classification === 'current-only' && reconciledMatch === 'matched') counts.currentOnlyPreserved += 1;
|
|
352
|
+
|
|
353
|
+
paths.push(deepFreeze({
|
|
354
|
+
path,
|
|
355
|
+
classification: classified.classification,
|
|
356
|
+
deletionCandidate: classified.deletionCandidate,
|
|
357
|
+
conflictCandidate: classified.conflictCandidate,
|
|
358
|
+
dispositionRequired: classified.dispositionRequired,
|
|
359
|
+
dispositionState,
|
|
360
|
+
disposition: disposition ? dispositionBindingReceipt(disposition) : null,
|
|
361
|
+
selectionState,
|
|
362
|
+
selectionSource,
|
|
363
|
+
base: optionalEntryIdentity(baseEntry),
|
|
364
|
+
incoming: optionalEntryIdentity(incomingEntry),
|
|
365
|
+
current: optionalEntryIdentity(currentEntry),
|
|
366
|
+
reconciled: optionalEntryIdentity(reconciledEntry),
|
|
367
|
+
incomingChange: changeKind(baseEntry, incomingEntry),
|
|
368
|
+
currentChange: changeKind(baseEntry, currentEntry),
|
|
369
|
+
reconciledMatch
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const expectedSnapshot = counts.unresolvedDisposition === 0 && paths.every((item) => item.selectionState !== 'invalid')
|
|
374
|
+
? createPortableWorkspaceSnapshot(expectedEntries, { basis: 'qualified-base-incoming-current-plus-explicit-dispositions' })
|
|
375
|
+
: null;
|
|
376
|
+
const state = counts.unresolvedDisposition
|
|
377
|
+
? 'disposition-required'
|
|
378
|
+
: counts.reconciledMismatch || unexpectedReconciledPaths.length || paths.some((item) => item.selectionState === 'invalid')
|
|
379
|
+
? 'reconciled-frontier-mismatch'
|
|
380
|
+
: 'reconciled';
|
|
381
|
+
|
|
382
|
+
return deepFreeze({
|
|
383
|
+
workspaceId,
|
|
384
|
+
state,
|
|
385
|
+
base: workspaceReceipt(base),
|
|
386
|
+
incoming: workspaceReceipt(incoming),
|
|
387
|
+
current: workspaceReceipt(current),
|
|
388
|
+
reconciled: workspaceReceipt(reconciled),
|
|
389
|
+
expectedSnapshot: expectedSnapshot ? snapshotReceipt(expectedSnapshot) : null,
|
|
390
|
+
paths: Object.freeze(paths),
|
|
391
|
+
unexpectedReconciledPaths: Object.freeze(unexpectedReconciledPaths),
|
|
392
|
+
counts: Object.freeze(counts),
|
|
393
|
+
basis: 'qualified-base-incoming-current-path-byte-sha256-plus-explicit-conflict-deletion-disposition-v1'
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function classifyPath(base, incoming, current) {
|
|
398
|
+
const incomingChanged = !sameOptionalEntry(base, incoming);
|
|
399
|
+
const currentChanged = !sameOptionalEntry(base, current);
|
|
400
|
+
const deletionCandidate = Boolean(base) && ((incomingChanged && !incoming) || (currentChanged && !current));
|
|
401
|
+
const conflictCandidate = incomingChanged && currentChanged && !sameOptionalEntry(incoming, current);
|
|
402
|
+
if (!incomingChanged && !currentChanged) return { classification: 'exact', countKey: 'exact', deletionCandidate: false, conflictCandidate: false, dispositionRequired: false };
|
|
403
|
+
if (conflictCandidate) return { classification: 'conflicting-overlap', countKey: 'conflictingOverlap', deletionCandidate, conflictCandidate: true, dispositionRequired: true };
|
|
404
|
+
if (deletionCandidate) return { classification: 'deletion-candidate', countKey: 'deletionCandidate', deletionCandidate: true, conflictCandidate: false, dispositionRequired: true };
|
|
405
|
+
if (incomingChanged && !currentChanged) return { classification: 'incoming-only', countKey: 'incomingOnly', deletionCandidate: false, conflictCandidate: false, dispositionRequired: false };
|
|
406
|
+
if (!incomingChanged && currentChanged) return { classification: 'current-only', countKey: 'currentOnly', deletionCandidate: false, conflictCandidate: false, dispositionRequired: false };
|
|
407
|
+
return { classification: 'same-result-concurrent', countKey: 'sameResultConcurrent', deletionCandidate: false, conflictCandidate: false, dispositionRequired: false };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function automaticSelection(classification, sides) {
|
|
411
|
+
if (classification === 'exact') return sides.baseEntry || sides.incomingEntry || sides.currentEntry || null;
|
|
412
|
+
if (classification === 'incoming-only') return sides.incomingEntry || null;
|
|
413
|
+
if (classification === 'current-only') return sides.currentEntry || null;
|
|
414
|
+
if (classification === 'same-result-concurrent') return sides.incomingEntry || sides.currentEntry || null;
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
function automaticSelectionSource(classification) {
|
|
418
|
+
if (classification === 'exact') return 'exact-common';
|
|
419
|
+
if (classification === 'incoming-only') return 'incoming';
|
|
420
|
+
if (classification === 'current-only') return 'current';
|
|
421
|
+
if (classification === 'same-result-concurrent') return 'same-result-concurrent';
|
|
422
|
+
return '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function resolveDisposition(disposition, sides) {
|
|
426
|
+
const action = disposition.action;
|
|
427
|
+
if (action === 'delete') return { qualified: true, entry: null, source: 'explicit-delete', message: '' };
|
|
428
|
+
if (action === 'incoming') return sides.incomingEntry ? { qualified: true, entry: sides.incomingEntry, source: 'explicit-incoming', message: '' } : { qualified: false, entry: null, source: '', message: 'Disposition selects incoming source, but incoming is absent. Use explicit delete to authorize absence.' };
|
|
429
|
+
if (action === 'current') return sides.currentEntry ? { qualified: true, entry: sides.currentEntry, source: 'explicit-current', message: '' } : { qualified: false, entry: null, source: '', message: 'Disposition selects current source, but current is absent. Use explicit delete to authorize absence.' };
|
|
430
|
+
if (action === 'base') return sides.baseEntry ? { qualified: true, entry: sides.baseEntry, source: 'explicit-base', message: '' } : { qualified: false, entry: null, source: '', message: 'Disposition selects base source, but base is absent.' };
|
|
431
|
+
if (action === 'reconciled') return sides.reconciledEntry ? { qualified: true, entry: sides.reconciledEntry, source: 'explicit-reconciled', message: '' } : { qualified: false, entry: null, source: '', message: 'Disposition selects reconciled source, but candidate reconciled path is absent. Use explicit delete to authorize absence.' };
|
|
432
|
+
return { qualified: false, entry: null, source: '', message: 'Disposition action is unsupported.' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeDispositions(value, findings) {
|
|
436
|
+
const raw = Array.isArray(value) ? value : Array.isArray(value?.dispositions) ? value.dispositions : [];
|
|
437
|
+
const out = [];
|
|
438
|
+
const seen = new Set();
|
|
439
|
+
for (const item of raw) {
|
|
440
|
+
const workspaceId = String(item?.workspaceId || item?.workspace || '').trim();
|
|
441
|
+
const path = normalizePath(item?.path || item?.innerPath || '');
|
|
442
|
+
const action = String(item?.action || item?.disposition || '').trim().toLowerCase();
|
|
443
|
+
if (!workspaceId || !path || unsafePath(path) || !DISPOSITION_ACTIONS.has(action)) {
|
|
444
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-invalid', 'Every disposition requires an explicit Workspace id, safe Workspace-relative path, and action incoming|current|base|reconciled|delete.', { workspaceId, path: String(item?.path || ''), action }));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const key = dispositionKey(workspaceId, path);
|
|
448
|
+
if (seen.has(key)) {
|
|
449
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconciliation.disposition-duplicate', 'Only one explicit disposition may be supplied for a Workspace/path candidate.', { workspaceId, path }));
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
seen.add(key);
|
|
453
|
+
out.push(deepFreeze({ workspaceId, path, action, ...(item?.note ? { note: String(item.note) } : {}) }));
|
|
454
|
+
}
|
|
455
|
+
out.sort((a, b) => a.workspaceId.localeCompare(b.workspaceId) || a.path.localeCompare(b.path));
|
|
456
|
+
return Object.freeze(out);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function aggregateProofCounts(workspaces) {
|
|
460
|
+
const counts = {
|
|
461
|
+
workspaces: workspaces.length,
|
|
462
|
+
exact: 0,
|
|
463
|
+
incomingOnly: 0,
|
|
464
|
+
currentOnly: 0,
|
|
465
|
+
sameResultConcurrent: 0,
|
|
466
|
+
deletionCandidate: 0,
|
|
467
|
+
conflictingOverlap: 0,
|
|
468
|
+
dispositionRequired: 0,
|
|
469
|
+
resolvedDisposition: 0,
|
|
470
|
+
unresolvedDisposition: 0,
|
|
471
|
+
reconciledMatch: 0,
|
|
472
|
+
reconciledMismatch: 0,
|
|
473
|
+
incomingOnlyPreserved: 0,
|
|
474
|
+
currentOnlyPreserved: 0,
|
|
475
|
+
pathCount: 0
|
|
476
|
+
};
|
|
477
|
+
for (const workspace of workspaces) {
|
|
478
|
+
for (const key of ['exact', 'incomingOnly', 'currentOnly', 'sameResultConcurrent', 'deletionCandidate', 'conflictingOverlap', 'dispositionRequired', 'resolvedDisposition', 'unresolvedDisposition', 'reconciledMatch', 'reconciledMismatch', 'incomingOnlyPreserved', 'currentOnlyPreserved']) counts[key] += Number(workspace.counts?.[key] || 0);
|
|
479
|
+
counts.pathCount += Number(workspace.counts?.totalPaths || 0);
|
|
480
|
+
}
|
|
481
|
+
return Object.freeze(counts);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function unavailableWorkspace(workspaceId, sourceSides, reconciled, missing, state = 'unavailable') {
|
|
485
|
+
return deepFreeze({
|
|
486
|
+
workspaceId,
|
|
487
|
+
state,
|
|
488
|
+
base: workspaceReceipt(sourceSides.base),
|
|
489
|
+
incoming: workspaceReceipt(sourceSides.incoming),
|
|
490
|
+
current: workspaceReceipt(sourceSides.current),
|
|
491
|
+
reconciled: workspaceReceipt(reconciled),
|
|
492
|
+
expectedSnapshot: null,
|
|
493
|
+
missing: Object.freeze([...(missing || [])]),
|
|
494
|
+
paths: Object.freeze([]),
|
|
495
|
+
unexpectedReconciledPaths: Object.freeze([]),
|
|
496
|
+
counts: Object.freeze({ exact: 0, incomingOnly: 0, currentOnly: 0, sameResultConcurrent: 0, deletionCandidate: 0, conflictingOverlap: 0, dispositionRequired: 0, resolvedDisposition: 0, unresolvedDisposition: 0, reconciledMatch: 0, reconciledMismatch: 0, incomingOnlyPreserved: 0, currentOnlyPreserved: 0, totalPaths: 0 })
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function ensureFrontier(frontier, label) {
|
|
501
|
+
if (frontier?.schema === PORTABLE_SOURCE_FRONTIER_SCHEMA_ID && Array.isArray(frontier.workspaces)) return createPortableSourceFrontier(frontier);
|
|
502
|
+
return createPortableSourceFrontier({
|
|
503
|
+
id: label,
|
|
504
|
+
state: 'qualification-error',
|
|
505
|
+
source: { kind: 'unavailable' },
|
|
506
|
+
workspaces: [],
|
|
507
|
+
findings: [portableFinding('error', `portable.source-frontier.reconciliation.${label}-input-invalid`, 'Reconciliation proof input is not a qualified normalized source frontier.', { side: label })]
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function frontierBindingReceipt(frontier) {
|
|
512
|
+
return deepFreeze({
|
|
513
|
+
schema: String(frontier?.schema || ''),
|
|
514
|
+
state: String(frontier?.state || ''),
|
|
515
|
+
id: String(frontier?.id || ''),
|
|
516
|
+
workspaces: Object.freeze((frontier?.workspaces || []).map((workspace) => Object.freeze({
|
|
517
|
+
workspaceId: String(workspace.workspaceId || ''),
|
|
518
|
+
state: String(workspace.state || ''),
|
|
519
|
+
qualification: String(workspace.qualification || ''),
|
|
520
|
+
...(workspace.snapshot ? { snapshot: snapshotReceipt(workspace.snapshot) } : {})
|
|
521
|
+
})))
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function workspaceReceipt(workspace) {
|
|
525
|
+
if (!workspace) return null;
|
|
526
|
+
return deepFreeze({ state: String(workspace.state || ''), qualification: String(workspace.qualification || ''), ...(workspace.snapshot ? { snapshot: snapshotReceipt(workspace.snapshot) } : {}) });
|
|
527
|
+
}
|
|
528
|
+
function snapshotReceipt(snapshot) {
|
|
529
|
+
return Object.freeze({ state: String(snapshot?.state || ''), entryCount: Number(snapshot?.entryCount || 0), totalBytes: Number(snapshot?.totalBytes || 0), fingerprint: String(snapshot?.fingerprint || ''), fingerprintMethod: String(snapshot?.fingerprintMethod || '') });
|
|
530
|
+
}
|
|
531
|
+
function workspaceBindingReceipt(workspace) {
|
|
532
|
+
return deepFreeze({
|
|
533
|
+
workspaceId: workspace.workspaceId,
|
|
534
|
+
state: workspace.state,
|
|
535
|
+
expectedSnapshot: workspace.expectedSnapshot,
|
|
536
|
+
reconciledSnapshot: workspace.reconciled?.snapshot || null,
|
|
537
|
+
counts: workspace.counts,
|
|
538
|
+
unresolvedPaths: Object.freeze((workspace.paths || []).filter((item) => item.dispositionState === 'required' || item.dispositionState === 'invalid').map((item) => item.path)),
|
|
539
|
+
mismatchedPaths: Object.freeze((workspace.paths || []).filter((item) => item.reconciledMatch === 'mismatch').map((item) => item.path)),
|
|
540
|
+
unexpectedReconciledPaths: workspace.unexpectedReconciledPaths || Object.freeze([])
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function dispositionBindingReceipt(disposition) { return Object.freeze({ workspaceId: disposition.workspaceId, path: disposition.path, action: disposition.action }); }
|
|
544
|
+
|
|
545
|
+
function unwrapProof(value) {
|
|
546
|
+
if (!value || typeof value !== 'object') return null;
|
|
547
|
+
if (value.schema === PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_SCHEMA_ID) return value;
|
|
548
|
+
if (value.schema === 'tiinex.portable.operation.result.v1' && value.resultSchema === PORTABLE_SOURCE_FRONTIER_RECONCILIATION_PROOF_SCHEMA_ID) return { ...value, schema: value.resultSchema };
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
function blockedManufactureQualification(findings, proof = null) {
|
|
552
|
+
const normalizedFindings = Object.freeze(dedupeFindings(findings));
|
|
553
|
+
return deepFreeze({ state: 'blocked', proofFingerprint: String(proof?.proofFingerprint || ''), findings: normalizedFindings, findingSummary: summarizePortableFindings(normalizedFindings), boundary: 'Manufacture reconciliation proof failed closed. No semantic merge or acceptance inference is performed.' });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function entryMap(snapshot) { return new Map((snapshot?.entries || []).map((entry) => [entry.path, entry])); }
|
|
557
|
+
function workspaceMap(frontier) { return new Map((frontier?.workspaces || []).map((workspace) => [workspace.workspaceId, workspace])); }
|
|
558
|
+
function sameEntry(left, right) { return Boolean(left && right && left.bytes === right.bytes && left.sha256 === right.sha256); }
|
|
559
|
+
function sameOptionalEntry(left, right) { return !left && !right ? true : Boolean(left && right && sameEntry(left, right)); }
|
|
560
|
+
function optionalEntryIdentity(entry) { return entry ? Object.freeze({ state: 'present', bytes: entry.bytes, sha256: entry.sha256 }) : Object.freeze({ state: 'absent' }); }
|
|
561
|
+
function changeKind(base, value) { if (sameOptionalEntry(base, value)) return 'unchanged'; if (!base && value) return 'added'; if (base && !value) return 'removed'; return 'byte-changed'; }
|
|
562
|
+
function dispositionKey(workspaceId, path) { return `${workspaceId}\u0000${path}`; }
|
|
563
|
+
function normalizePath(value = '') { return String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, ''); }
|
|
564
|
+
function unsafePath(value = '') { return !value || value.includes('\u0000') || /^[\/]/.test(value) || /^[A-Za-z]:[\/]/.test(value) || value.endsWith('/') || value.split('/').some((part) => !part || part === '.' || part === '..'); }
|
|
565
|
+
function positiveInteger(value, fallback) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }
|
|
566
|
+
function stableJson(value) { return JSON.stringify(sortJson(value)); }
|
|
567
|
+
function sortJson(value) { if (Array.isArray(value)) return value.map(sortJson); if (!value || typeof value !== 'object') return value; return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]).filter(([, child]) => typeof child !== 'undefined')); }
|
|
568
|
+
function dedupeFindings(findings = []) { const seen = new Set(); const out = []; for (const item of findings) { const key = `${item?.severity || ''}\u0000${item?.code || ''}\u0000${item?.message || ''}\u0000${item?.workspaceId || ''}\u0000${item?.path || ''}`; if (seen.has(key)) continue; seen.add(key); out.push(Object.freeze({ ...item })); } return out; }
|
|
569
|
+
function deepFreeze(value) { if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value; for (const child of Object.values(value)) deepFreeze(child); return Object.freeze(value); }
|
|
@@ -265,6 +265,9 @@ function renderBody({ title, plan, document, values, sections, allowIncomplete }
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
function renderGenericDraft({ schemaId, title, summary, createdAt, why, currentStatus, parentRecord, bodyMarkdown }) {
|
|
268
|
+
// Bounded exception: this renderer creates local drafts from explicitly supplied readable
|
|
269
|
+
// schema material, so its schema links are truthful draft-local relative locators rather
|
|
270
|
+
// than claims that an immutable published canonical target exists.
|
|
268
271
|
const lines = [
|
|
269
272
|
'# Continuity Context',
|
|
270
273
|
'',
|
|
@@ -18,7 +18,7 @@ export function validatePortableDraft(input = {}, options = {}) {
|
|
|
18
18
|
const parsed = safeParse(markdown);
|
|
19
19
|
const declaredSchema = String(parsed.envelope?.current?.schema?.id || record.schemaId || '').trim();
|
|
20
20
|
const runtimeProjection = portableRuntimeValidationContractForSchema(requestedSchema || declaredSchema);
|
|
21
|
-
const audit = runAudit({ record, markdown, schemaReferenceAuthorities: input.schemaReferenceAuthorities || null, validationContractOverride: runtimeProjection.state === 'qualified' ? runtimeProjection.compiledContract : null });
|
|
21
|
+
const audit = runAudit({ record, markdown, schemaReferenceAuthorities: input.schemaReferenceAuthorities || null, schemaReferenceContext: 'candidate', validationContractOverride: runtimeProjection.state === 'qualified' ? runtimeProjection.compiledContract : null });
|
|
22
22
|
if (requestedSchema && declaredSchema !== requestedSchema) findings.push(portableFinding('error', 'portable.draft.schema.mismatch', 'Draft Current Schema does not match the requested schema.', { requestedSchema, declaredSchema, ref: path }));
|
|
23
23
|
const sharedParserQuirks = detectSharedParserQuirks(markdown, parsed, audit, path);
|
|
24
24
|
const suppressedAuditCodes = new Set(sharedParserQuirks.flatMap((quirk) => quirk.suppressedCodes || []));
|
|
@@ -28,8 +28,7 @@ function projectDocument(record = {}, records = [], lineageInspection = null) {
|
|
|
28
28
|
const markdown = String(record.markdown || '');
|
|
29
29
|
const recordPath = norm(record.path || record.id || '');
|
|
30
30
|
const lineageFindings = findingsForPath(lineageInspection?.findings || [], recordPath);
|
|
31
|
-
const
|
|
32
|
-
const sharedFindings = [...(audit.findings || []), ...lineageFindings, ...referenceFindings];
|
|
31
|
+
const sharedFindings = [...(audit.findings || []), ...lineageFindings];
|
|
33
32
|
const diagnostics = sharedFindings
|
|
34
33
|
.filter((item) => item.severity === 'error' || item.severity === 'warning')
|
|
35
34
|
.map((finding) => projectDiagnostic(finding, markdown));
|
|
@@ -68,28 +67,6 @@ function projectDocument(record = {}, records = [], lineageInspection = null) {
|
|
|
68
67
|
});
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
function schemaReferenceCompositionFindings(audit = {}, recordPath = '') {
|
|
72
|
-
const envelope = audit?.parsed?.envelope || {};
|
|
73
|
-
const references = [
|
|
74
|
-
{ field: 'Envelope Schema', value: envelope.envelopeSchema || {} },
|
|
75
|
-
{ field: 'Current Schema', value: envelope.current?.schema || {} },
|
|
76
|
-
{ field: 'Parent Schema', value: envelope.parent?.schema || {} }
|
|
77
|
-
].filter((entry) => String(entry.value?.id || '').trim());
|
|
78
|
-
const exactPublishedStyle = references.some((entry) => entry.value?.form === 'markdown-link' && /^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[0-9a-f]{40}\//i.test(String(entry.value?.target || '')));
|
|
79
|
-
if (!exactPublishedStyle) return [];
|
|
80
|
-
const findings = [];
|
|
81
|
-
for (const entry of references) {
|
|
82
|
-
if (entry.value?.form !== 'plain-schema-id') continue;
|
|
83
|
-
const schemaId = String(entry.value?.id || '').trim();
|
|
84
|
-
findings.push(portableFinding('warning', 'portable.editor.schema-reference.canonical-target-available', `${entry.field} uses only a schema id while the same artifact otherwise uses exact immutable schema locators. This mixed representation requires explicit qualification instead of being treated as clean.`, {
|
|
85
|
-
ref: recordPath,
|
|
86
|
-
params: { field: entry.field, schemaId },
|
|
87
|
-
fixability: 'manual-or-holistic-repair-required'
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
return findings;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
70
|
function findingsForPath(findings = [], path = '') {
|
|
94
71
|
const wanted = norm(path);
|
|
95
72
|
return (findings || []).filter((finding) => norm(finding?.evidencePath || finding?.ref || '') === wanted);
|
|
@@ -102,7 +79,7 @@ function qualifyReplacementAgainstSharedGuardrails(record = {}, records = [], re
|
|
|
102
79
|
const replacementRecord = replacedRecords.find((item) => norm(item.path || item.id || '') === focusPath);
|
|
103
80
|
if (!replacementRecord) return freeze({ state: 'unavailable', reason: 'focused-record-unavailable' });
|
|
104
81
|
const replacementAudit = auditPortableRecord(replacementRecord, { requireExactSchemaAuthority: true });
|
|
105
|
-
const auditBlockers = [...(replacementAudit.findings || [])
|
|
82
|
+
const auditBlockers = [...(replacementAudit.findings || [])].filter((item) => item.severity === 'error' || item.severity === 'warning');
|
|
106
83
|
if (auditBlockers.length) return freeze({ state: 'blocked', reason: 'replacement-shared-audit-not-clean', blockerCodes: auditBlockers.map((item) => String(item.code || '')) });
|
|
107
84
|
|
|
108
85
|
const before = inspectPortableLineageIntegrity({ records });
|