@tiinex/core 0.3.0 → 0.4.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 +8 -6
- package/src/public/index.js +21 -0
- package/src/public/node.js +1 -0
- package/src/release/plan.mjs +18 -5
- package/src/release/run.mjs +7 -5
- package/src/tooling/portable/adapters/cli/cli.command-input.js +3 -0
- package/src/tooling/portable/adapters/cli/cli.common-output.js +23 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +2 -0
- package/src/tooling/portable/adapters/cli/cli.help.js +18 -2
- package/src/tooling/portable/adapters/cli/cli.material-policy.js +0 -1
- package/src/tooling/portable/adapters/cli/cli.operator-bridge.js +0 -13
- package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
- package/src/tooling/portable/adapters/cli/cli.source-frontier-comparison.js +50 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +14 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.packageParent.js +80 -16
- package/src/tooling/portable/adapters/node/handoff.manufacture.requirements.js +44 -11
- package/src/tooling/portable/adapters/node/handoff.manufacture.scope.js +96 -1
- package/src/tooling/portable/adapters/node/sourceFrontierComparison.js +194 -0
- package/src/tooling/portable/comparison/sourceFrontierComparison.js +501 -0
- package/src/tooling/portable/grounding/grounding.readiness.js +8 -4
- package/src/tooling/portable/grounding/grounding.readiness.support.js +63 -1
- package/src/tooling/portable/handoff/contextAudit.js +21 -1
- package/src/tooling/portable/handoff/materialClosure.descriptor.js +1 -1
- package/src/tooling/portable/handoff/materialClosure.materials.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.artifacts.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.inspect.helpers.js +6 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +93 -19
- package/src/tooling/portable/handoff/recipientV2.packageV1.contract.js +30 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +8 -2
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +122 -10
- package/src/tooling/portable/handoff/recipientV2.packageV1.js +2 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.secure.js +87 -0
- package/src/tooling/portable/handoff/recipientV2.packageV1.shared.js +9 -1
- package/src/tooling/portable/handoff/recipientV2.topology.js +2 -2
- package/src/tooling/portable/handoff/recipientV2.topology.materials.js +9 -1
- package/src/tooling/portable/handoff/transportEnvelopeV1.js +76 -0
- package/src/tooling/portable/index.js +4 -0
- package/src/tooling/portable/operation.catalog.js +8 -0
- package/src/tooling/portable/operation.catalog.package.js +0 -8
- package/src/transport/secureTransportV1.js +339 -0
- package/src/tooling/portable/handoff/sourceFrontierComparison.js +0 -186
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { sha256Hex, utf8Bytes } from '../../../export/package.bytes.js';
|
|
2
|
+
import { portableFinding, summarizePortableFindings } from '../findings.js';
|
|
3
|
+
|
|
4
|
+
export const PORTABLE_SOURCE_FRONTIER_SCHEMA_ID = 'tiinex.portable.source-frontier.v1';
|
|
5
|
+
export const PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID = 'tiinex.portable.source-frontier-comparison.v1';
|
|
6
|
+
export const PORTABLE_SOURCE_FRONTIER_SUMMARY_SCHEMA_ID = 'tiinex.portable.source-frontier-comparison-summary.v1';
|
|
7
|
+
|
|
8
|
+
const QUALIFIED = 'qualified';
|
|
9
|
+
const WORKSPACE_NONQUALIFIED_STATES = new Set(['locked', 'unavailable', 'qualification-error']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize already-selected exact Workspace source into the host-neutral comparison model.
|
|
13
|
+
* This function performs no filesystem/archive discovery and grants no semantic authority.
|
|
14
|
+
*/
|
|
15
|
+
export function createPortableSourceFrontier(input = {}) {
|
|
16
|
+
const findings = [...(input.findings || [])];
|
|
17
|
+
const workspaces = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const raw of input.workspaces || []) {
|
|
20
|
+
const workspaceId = String(raw?.workspaceId || raw?.id || '').trim();
|
|
21
|
+
if (!workspaceId) {
|
|
22
|
+
findings.push(portableFinding('error', 'portable.source-frontier.workspace-id.required', 'Every normalized source-frontier Workspace requires an explicit Workspace id.'));
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (seen.has(workspaceId)) {
|
|
26
|
+
findings.push(portableFinding('error', 'portable.source-frontier.workspace-id.duplicate', 'Normalized source frontier contains a duplicate Workspace id.', { workspaceId }));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
seen.add(workspaceId);
|
|
30
|
+
workspaces.push(normalizeWorkspace(raw, workspaceId, findings));
|
|
31
|
+
}
|
|
32
|
+
workspaces.sort((a, b) => a.workspaceId.localeCompare(b.workspaceId));
|
|
33
|
+
const explicitState = String(input.state || '').trim();
|
|
34
|
+
const state = findings.some((item) => item?.severity === 'error')
|
|
35
|
+
? 'qualification-error'
|
|
36
|
+
: explicitState === 'qualification-error'
|
|
37
|
+
? explicitState
|
|
38
|
+
: 'qualified';
|
|
39
|
+
const normalizedFindings = Object.freeze(findings.map((item) => Object.freeze({ ...item })));
|
|
40
|
+
return deepFreeze({
|
|
41
|
+
schema: PORTABLE_SOURCE_FRONTIER_SCHEMA_ID,
|
|
42
|
+
state,
|
|
43
|
+
id: String(input.id || input.label || '').trim(),
|
|
44
|
+
source: serializableSource(input.source || {}),
|
|
45
|
+
workspaces: Object.freeze(workspaces),
|
|
46
|
+
findings: normalizedFindings,
|
|
47
|
+
findingSummary: summarizePortableFindings(normalizedFindings),
|
|
48
|
+
boundary: String(input.boundary || 'Exact source-byte frontier only. Workspace/path equality does not establish semantic equivalence, authority, acceptance, or merge disposition.')
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createPortableWorkspaceSnapshot(entries = [], evidence = {}) {
|
|
53
|
+
const findings = [];
|
|
54
|
+
const normalized = normalizeEntries(entries, findings);
|
|
55
|
+
if (findings.length) return deepFreeze({ state: 'qualification-error', entries: Object.freeze([]), findings: Object.freeze(findings) });
|
|
56
|
+
const totalBytes = normalized.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
57
|
+
const fingerprint = snapshotFingerprint(normalized);
|
|
58
|
+
return deepFreeze({
|
|
59
|
+
state: QUALIFIED,
|
|
60
|
+
entryCount: normalized.length,
|
|
61
|
+
totalBytes,
|
|
62
|
+
fingerprint,
|
|
63
|
+
fingerprintMethod: 'sha256-path-bytes-sha256-v1',
|
|
64
|
+
entries: Object.freeze(normalized),
|
|
65
|
+
evidence: Object.freeze(serializableObject(evidence)),
|
|
66
|
+
findings: Object.freeze([])
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Two-way exact source-frontier comparison. */
|
|
71
|
+
export function comparePortableSourceFrontiers(input = {}) {
|
|
72
|
+
const left = ensureFrontier(input.left, 'left');
|
|
73
|
+
const right = ensureFrontier(input.right, 'right');
|
|
74
|
+
const findings = [...(left.findings || []), ...(right.findings || [])];
|
|
75
|
+
if (left.state === 'qualification-error' || right.state === 'qualification-error') {
|
|
76
|
+
findings.push(portableFinding('error', 'portable.source-frontier.compare.input-unqualified', 'Source-frontier comparison requires qualified normalized inputs; one or both input frontiers are unqualified.'));
|
|
77
|
+
}
|
|
78
|
+
const workspaces = compareWorkspaceUnion(left, right);
|
|
79
|
+
const comparisonState = aggregatePairState(workspaces, findings);
|
|
80
|
+
const normalizedFindings = Object.freeze(dedupeFindings(findings));
|
|
81
|
+
const result = deepFreeze({
|
|
82
|
+
schema: PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID,
|
|
83
|
+
mode: 'two-way',
|
|
84
|
+
status: normalizedFindings.some((item) => item.severity === 'error') ? 'blocked' : 'ready',
|
|
85
|
+
state: comparisonState,
|
|
86
|
+
inputs: Object.freeze({ left: frontierReceipt(left), right: frontierReceipt(right) }),
|
|
87
|
+
workspaces: Object.freeze(workspaces),
|
|
88
|
+
counts: pairCounts(workspaces),
|
|
89
|
+
findings: normalizedFindings,
|
|
90
|
+
findingSummary: summarizePortableFindings(normalizedFindings),
|
|
91
|
+
operationBoundary: comparisonBoundary(),
|
|
92
|
+
boundary: 'Read-only exact source comparison. Hash/path evidence is used only for byte-source reconciliation; no semantic diff, merge, source mutation, remote acquisition, acceptance, or authority is performed.'
|
|
93
|
+
});
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Three-way base -> incoming/current reconciliation evidence without merge behavior. */
|
|
98
|
+
export function reconcilePortableSourceFrontiers(input = {}) {
|
|
99
|
+
const base = ensureFrontier(input.base, 'base');
|
|
100
|
+
const incoming = ensureFrontier(input.incoming, 'incoming');
|
|
101
|
+
const current = ensureFrontier(input.current, 'current');
|
|
102
|
+
const findings = [...(base.findings || []), ...(incoming.findings || []), ...(current.findings || [])];
|
|
103
|
+
if ([base, incoming, current].some((frontier) => frontier.state === 'qualification-error')) {
|
|
104
|
+
findings.push(portableFinding('error', 'portable.source-frontier.reconcile.input-unqualified', 'Three-way source-frontier reconciliation requires qualified normalized inputs.'));
|
|
105
|
+
}
|
|
106
|
+
const workspaces = reconcileWorkspaceUnion(base, incoming, current);
|
|
107
|
+
const normalizedFindings = Object.freeze(dedupeFindings(findings));
|
|
108
|
+
const result = deepFreeze({
|
|
109
|
+
schema: PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID,
|
|
110
|
+
mode: 'three-way',
|
|
111
|
+
status: normalizedFindings.some((item) => item.severity === 'error') ? 'blocked' : 'ready',
|
|
112
|
+
state: aggregateThreeWayState(workspaces, normalizedFindings),
|
|
113
|
+
inputs: Object.freeze({ base: frontierReceipt(base), incoming: frontierReceipt(incoming), current: frontierReceipt(current) }),
|
|
114
|
+
workspaces: Object.freeze(workspaces),
|
|
115
|
+
counts: threeWayCounts(workspaces),
|
|
116
|
+
findings: normalizedFindings,
|
|
117
|
+
findingSummary: summarizePortableFindings(normalizedFindings),
|
|
118
|
+
operationBoundary: comparisonBoundary(),
|
|
119
|
+
boundary: 'Read-only three-way byte/path reconciliation evidence. Conflict candidates are exact source overlaps only; this result performs no merge and makes no semantic or acceptance decision.'
|
|
120
|
+
});
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function compareOrReconcilePortableSourceFrontiers(input = {}) {
|
|
125
|
+
if (input.base || input.incoming || input.current) {
|
|
126
|
+
if (!input.base || !input.incoming || !input.current) return blockedShape('three-way', 'portable.source-frontier.reconcile.inputs-required', 'Three-way reconciliation requires base, incoming, and current frontiers.');
|
|
127
|
+
return reconcilePortableSourceFrontiers(input);
|
|
128
|
+
}
|
|
129
|
+
if (!input.left || !input.right) return blockedShape('two-way', 'portable.source-frontier.compare.inputs-required', 'Two-way comparison requires left and right frontiers.');
|
|
130
|
+
return comparePortableSourceFrontiers(input);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Compact human/LLM projection; full machine receipt remains the source of path truth. */
|
|
134
|
+
export function projectPortableSourceFrontierComparisonSummary(result = {}, options = {}) {
|
|
135
|
+
const maxPaths = positiveInteger(options.maxPaths, 20);
|
|
136
|
+
const workspaces = (result.workspaces || []).map((workspace) => projectWorkspaceSummary(workspace, maxPaths));
|
|
137
|
+
return deepFreeze({
|
|
138
|
+
schema: PORTABLE_SOURCE_FRONTIER_SUMMARY_SCHEMA_ID,
|
|
139
|
+
resultSchema: String(result.schema || ''),
|
|
140
|
+
mode: String(result.mode || ''),
|
|
141
|
+
status: String(result.status || ''),
|
|
142
|
+
state: String(result.state || ''),
|
|
143
|
+
inputs: result.inputs ? deepFreeze(Object.fromEntries(Object.entries(result.inputs).map(([key, value]) => [key, projectFrontierSummary(value)]))) : null,
|
|
144
|
+
workspaces: Object.freeze(workspaces),
|
|
145
|
+
counts: result.counts ? Object.freeze({ ...result.counts }) : null,
|
|
146
|
+
findingSummary: result.findingSummary || summarizePortableFindings(result.findings || []),
|
|
147
|
+
actionableFindings: Object.freeze((result.findings || []).filter((item) => item.severity === 'error' || item.severity === 'warning').slice(0, 20).map((item) => Object.freeze({ ...item }))),
|
|
148
|
+
fullReceiptAvailable: true,
|
|
149
|
+
boundary: 'Compact projection only. Path lists are bounded here; use the full machine receipt for complete deterministic deltas and three-way classifications.'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeWorkspace(raw, workspaceId, findings) {
|
|
154
|
+
const requestedState = String(raw?.state || (raw?.snapshot ? QUALIFIED : '')).trim();
|
|
155
|
+
if (WORKSPACE_NONQUALIFIED_STATES.has(requestedState)) {
|
|
156
|
+
return deepFreeze({
|
|
157
|
+
workspaceId,
|
|
158
|
+
state: requestedState,
|
|
159
|
+
qualification: String(raw?.qualification || requestedState),
|
|
160
|
+
reason: String(raw?.reason || ''),
|
|
161
|
+
source: serializableSource(raw?.source || {}),
|
|
162
|
+
snapshot: null
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const snapshot = raw?.snapshot?.state === QUALIFIED && Array.isArray(raw.snapshot.entries)
|
|
166
|
+
? createPortableWorkspaceSnapshot(raw.snapshot.entries, raw.snapshot.evidence || raw.evidence || {})
|
|
167
|
+
: createPortableWorkspaceSnapshot(raw?.entries || [], raw?.evidence || {});
|
|
168
|
+
if (snapshot.state !== QUALIFIED) {
|
|
169
|
+
findings.push(...(snapshot.findings || []));
|
|
170
|
+
return deepFreeze({ workspaceId, state: 'qualification-error', qualification: 'unqualified-snapshot', reason: 'snapshot-unqualified', source: serializableSource(raw?.source || {}), snapshot: null });
|
|
171
|
+
}
|
|
172
|
+
return deepFreeze({
|
|
173
|
+
workspaceId,
|
|
174
|
+
state: QUALIFIED,
|
|
175
|
+
qualification: String(raw?.qualification || 'qualified-exact-source'),
|
|
176
|
+
source: serializableSource(raw?.source || {}),
|
|
177
|
+
snapshot
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeEntries(entries, findings) {
|
|
182
|
+
const out = [];
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const raw of entries || []) {
|
|
185
|
+
const rawPath = String(raw?.path || raw?.innerPath || '').trim();
|
|
186
|
+
const path = normalizePath(rawPath);
|
|
187
|
+
if (unsafeRawPath(rawPath) || !path || unsafePath(path)) {
|
|
188
|
+
findings.push(portableFinding('error', 'portable.source-frontier.entry.path-invalid', 'Comparable source entry has an unsafe or empty Workspace-relative path.', { path: rawPath }));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (seen.has(path)) {
|
|
192
|
+
findings.push(portableFinding('error', 'portable.source-frontier.entry.path-duplicate', 'Comparable source snapshot contains a duplicate normalized path.', { path }));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
seen.add(path);
|
|
196
|
+
const bytes = Number(raw?.bytes ?? raw?.size ?? 0);
|
|
197
|
+
const sha256 = String(raw?.sha256 || '').toLowerCase();
|
|
198
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/.test(sha256)) {
|
|
199
|
+
findings.push(portableFinding('error', 'portable.source-frontier.entry.identity-invalid', 'Comparable source entry requires exact non-negative byte size and SHA-256 evidence.', { path, bytes: raw?.bytes ?? raw?.size ?? null, sha256: String(raw?.sha256 || '') }));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out.push(Object.freeze({ path, bytes, sha256 }));
|
|
203
|
+
}
|
|
204
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function compareWorkspaceUnion(left, right) {
|
|
209
|
+
const leftById = workspaceMap(left);
|
|
210
|
+
const rightById = workspaceMap(right);
|
|
211
|
+
const ids = [...new Set([...leftById.keys(), ...rightById.keys()])].sort();
|
|
212
|
+
return ids.map((workspaceId) => compareWorkspace(workspaceId, leftById.get(workspaceId), rightById.get(workspaceId)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function compareWorkspace(workspaceId, left, right) {
|
|
216
|
+
if (!left) return deepFreeze({ workspaceId, state: 'only-right', left: null, right: workspaceSide(right), delta: null });
|
|
217
|
+
if (!right) return deepFreeze({ workspaceId, state: 'only-left', left: workspaceSide(left), right: null, delta: null });
|
|
218
|
+
const precedence = exceptionalPairState(left.state, right.state);
|
|
219
|
+
if (precedence) return deepFreeze({ workspaceId, state: precedence, left: workspaceSide(left, { opaque: precedence === 'locked' }), right: workspaceSide(right, { opaque: precedence === 'locked' }), delta: null });
|
|
220
|
+
const leftSnapshot = left.snapshot;
|
|
221
|
+
const rightSnapshot = right.snapshot;
|
|
222
|
+
if (sameSnapshot(leftSnapshot, rightSnapshot)) {
|
|
223
|
+
return deepFreeze({ workspaceId, state: 'exact', left: workspaceSide(left), right: workspaceSide(right), delta: Object.freeze({ added: Object.freeze([]), removed: Object.freeze([]), byteChanged: Object.freeze([]), counts: Object.freeze({ added: 0, removed: 0, byteChanged: 0, total: 0 }), basis: 'qualified-snapshot-fingerprint-fast-path' }) });
|
|
224
|
+
}
|
|
225
|
+
const delta = snapshotDelta(leftSnapshot, rightSnapshot);
|
|
226
|
+
return deepFreeze({ workspaceId, state: delta.counts.total ? 'changed' : 'exact', left: workspaceSide(left), right: workspaceSide(right), delta });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function snapshotDelta(left, right) {
|
|
230
|
+
const leftMap = entryMap(left);
|
|
231
|
+
const rightMap = entryMap(right);
|
|
232
|
+
const added = [];
|
|
233
|
+
const removed = [];
|
|
234
|
+
const byteChanged = [];
|
|
235
|
+
for (const path of [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort()) {
|
|
236
|
+
const l = leftMap.get(path);
|
|
237
|
+
const r = rightMap.get(path);
|
|
238
|
+
if (!l) added.push(entryIdentity(r));
|
|
239
|
+
else if (!r) removed.push(entryIdentity(l));
|
|
240
|
+
else if (!sameEntry(l, r)) byteChanged.push(Object.freeze({ path, left: entryIdentity(l), right: entryIdentity(r) }));
|
|
241
|
+
}
|
|
242
|
+
return deepFreeze({
|
|
243
|
+
added: Object.freeze(added),
|
|
244
|
+
removed: Object.freeze(removed),
|
|
245
|
+
byteChanged: Object.freeze(byteChanged),
|
|
246
|
+
counts: Object.freeze({ added: added.length, removed: removed.length, byteChanged: byteChanged.length, total: added.length + removed.length + byteChanged.length }),
|
|
247
|
+
basis: 'workspace-relative-path-plus-qualified-byte-size-and-sha256'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function reconcileWorkspaceUnion(base, incoming, current) {
|
|
252
|
+
const baseById = workspaceMap(base);
|
|
253
|
+
const incomingById = workspaceMap(incoming);
|
|
254
|
+
const currentById = workspaceMap(current);
|
|
255
|
+
const ids = [...new Set([...baseById.keys(), ...incomingById.keys(), ...currentById.keys()])].sort();
|
|
256
|
+
return ids.map((workspaceId) => reconcileWorkspace(workspaceId, baseById.get(workspaceId), incomingById.get(workspaceId), currentById.get(workspaceId)));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function reconcileWorkspace(workspaceId, base, incoming, current) {
|
|
260
|
+
const sides = { base, incoming, current };
|
|
261
|
+
const present = Object.values(sides).filter(Boolean);
|
|
262
|
+
if (present.length !== 3) {
|
|
263
|
+
const missing = Object.entries(sides).filter(([, value]) => !value).map(([key]) => key);
|
|
264
|
+
return deepFreeze({ workspaceId, state: 'unavailable', base: workspaceSide(base), incoming: workspaceSide(incoming), current: workspaceSide(current), missing: Object.freeze(missing), paths: Object.freeze([]), counts: emptyThreeWayCounts() });
|
|
265
|
+
}
|
|
266
|
+
const exceptional = exceptionalThreeWayState(base.state, incoming.state, current.state);
|
|
267
|
+
if (exceptional) return deepFreeze({ workspaceId, state: exceptional, base: workspaceSide(base, { opaque: exceptional === 'locked' }), incoming: workspaceSide(incoming, { opaque: exceptional === 'locked' }), current: workspaceSide(current, { opaque: exceptional === 'locked' }), paths: Object.freeze([]), counts: emptyThreeWayCounts() });
|
|
268
|
+
|
|
269
|
+
const baseMap = entryMap(base.snapshot);
|
|
270
|
+
const incomingMap = entryMap(incoming.snapshot);
|
|
271
|
+
const currentMap = entryMap(current.snapshot);
|
|
272
|
+
const paths = [];
|
|
273
|
+
const counts = { incomingOnly: 0, currentOnly: 0, sameResultConcurrent: 0, conflictCandidate: 0, total: 0 };
|
|
274
|
+
for (const path of [...new Set([...baseMap.keys(), ...incomingMap.keys(), ...currentMap.keys()])].sort()) {
|
|
275
|
+
const b = baseMap.get(path) || null;
|
|
276
|
+
const i = incomingMap.get(path) || null;
|
|
277
|
+
const c = currentMap.get(path) || null;
|
|
278
|
+
const incomingChanged = !sameOptionalEntry(b, i);
|
|
279
|
+
const currentChanged = !sameOptionalEntry(b, c);
|
|
280
|
+
if (!incomingChanged && !currentChanged) continue;
|
|
281
|
+
let classification;
|
|
282
|
+
if (incomingChanged && !currentChanged) classification = 'incoming-only';
|
|
283
|
+
else if (!incomingChanged && currentChanged) classification = 'current-only';
|
|
284
|
+
else if (sameOptionalEntry(i, c)) classification = 'same-result-concurrent';
|
|
285
|
+
else classification = 'conflict-candidate';
|
|
286
|
+
if (classification === 'incoming-only') counts.incomingOnly += 1;
|
|
287
|
+
else if (classification === 'current-only') counts.currentOnly += 1;
|
|
288
|
+
else if (classification === 'same-result-concurrent') counts.sameResultConcurrent += 1;
|
|
289
|
+
else counts.conflictCandidate += 1;
|
|
290
|
+
counts.total += 1;
|
|
291
|
+
paths.push(deepFreeze({
|
|
292
|
+
path,
|
|
293
|
+
classification,
|
|
294
|
+
base: optionalEntryIdentity(b),
|
|
295
|
+
incoming: optionalEntryIdentity(i),
|
|
296
|
+
current: optionalEntryIdentity(c),
|
|
297
|
+
incomingChange: changeKind(b, i),
|
|
298
|
+
currentChange: changeKind(b, c)
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
return deepFreeze({
|
|
302
|
+
workspaceId,
|
|
303
|
+
state: paths.length ? (counts.conflictCandidate ? 'conflict-candidate' : 'changed') : 'exact',
|
|
304
|
+
base: workspaceSide(base), incoming: workspaceSide(incoming), current: workspaceSide(current),
|
|
305
|
+
paths: Object.freeze(paths), counts: Object.freeze(counts),
|
|
306
|
+
basis: 'base-relative-qualified-path-byte-sha256-classification-v1'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function exceptionalPairState(leftState, rightState) {
|
|
311
|
+
const states = [leftState, rightState];
|
|
312
|
+
if (states.includes('qualification-error')) return 'qualification-error';
|
|
313
|
+
if (states.includes('locked')) return 'locked';
|
|
314
|
+
if (states.includes('unavailable')) return 'unavailable';
|
|
315
|
+
if (states.some((state) => state !== QUALIFIED)) return 'qualification-error';
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function exceptionalThreeWayState(...states) {
|
|
320
|
+
if (states.includes('qualification-error')) return 'qualification-error';
|
|
321
|
+
if (states.includes('locked')) return 'locked';
|
|
322
|
+
if (states.includes('unavailable')) return 'unavailable';
|
|
323
|
+
if (states.some((state) => state !== QUALIFIED)) return 'qualification-error';
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function aggregatePairState(workspaces, findings) {
|
|
328
|
+
if (findings.some((item) => item.severity === 'error') || workspaces.some((item) => item.state === 'qualification-error')) return 'qualification-error';
|
|
329
|
+
const states = new Set(workspaces.map((item) => item.state));
|
|
330
|
+
if (!workspaces.length) return 'exact';
|
|
331
|
+
if (states.size === 1) return workspaces[0].state;
|
|
332
|
+
if ([...states].some((state) => state === 'changed' || state === 'only-left' || state === 'only-right')) return states.has('locked') || states.has('unavailable') ? 'mixed' : 'changed';
|
|
333
|
+
if (states.has('locked') || states.has('unavailable')) return 'mixed';
|
|
334
|
+
return 'mixed';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function aggregateThreeWayState(workspaces, findings) {
|
|
338
|
+
if (findings.some((item) => item.severity === 'error') || workspaces.some((item) => item.state === 'qualification-error')) return 'qualification-error';
|
|
339
|
+
if (workspaces.some((item) => item.state === 'conflict-candidate')) return 'conflict-candidate';
|
|
340
|
+
if (workspaces.some((item) => item.state === 'changed')) return workspaces.some((item) => item.state === 'locked' || item.state === 'unavailable') ? 'mixed' : 'changed';
|
|
341
|
+
if (workspaces.some((item) => item.state === 'locked' || item.state === 'unavailable')) return 'mixed';
|
|
342
|
+
return 'exact';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function pairCounts(workspaces) {
|
|
346
|
+
const counts = { exact: 0, changed: 0, onlyLeft: 0, onlyRight: 0, locked: 0, unavailable: 0, qualificationError: 0, workspaces: workspaces.length, pathChanges: 0 };
|
|
347
|
+
for (const workspace of workspaces) {
|
|
348
|
+
if (workspace.state === 'exact') counts.exact += 1;
|
|
349
|
+
else if (workspace.state === 'changed') counts.changed += 1;
|
|
350
|
+
else if (workspace.state === 'only-left') counts.onlyLeft += 1;
|
|
351
|
+
else if (workspace.state === 'only-right') counts.onlyRight += 1;
|
|
352
|
+
else if (workspace.state === 'locked') counts.locked += 1;
|
|
353
|
+
else if (workspace.state === 'unavailable') counts.unavailable += 1;
|
|
354
|
+
else if (workspace.state === 'qualification-error') counts.qualificationError += 1;
|
|
355
|
+
counts.pathChanges += Number(workspace.delta?.counts?.total || 0);
|
|
356
|
+
}
|
|
357
|
+
return Object.freeze(counts);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function threeWayCounts(workspaces) {
|
|
361
|
+
const counts = { workspaces: workspaces.length, exact: 0, changed: 0, conflictCandidateWorkspaces: 0, locked: 0, unavailable: 0, qualificationError: 0, incomingOnly: 0, currentOnly: 0, sameResultConcurrent: 0, conflictCandidate: 0, pathChanges: 0 };
|
|
362
|
+
for (const workspace of workspaces) {
|
|
363
|
+
if (workspace.state === 'exact') counts.exact += 1;
|
|
364
|
+
else if (workspace.state === 'changed') counts.changed += 1;
|
|
365
|
+
else if (workspace.state === 'conflict-candidate') counts.conflictCandidateWorkspaces += 1;
|
|
366
|
+
else if (workspace.state === 'locked') counts.locked += 1;
|
|
367
|
+
else if (workspace.state === 'unavailable') counts.unavailable += 1;
|
|
368
|
+
else if (workspace.state === 'qualification-error') counts.qualificationError += 1;
|
|
369
|
+
for (const key of ['incomingOnly', 'currentOnly', 'sameResultConcurrent', 'conflictCandidate']) counts[key] += Number(workspace.counts?.[key] || 0);
|
|
370
|
+
counts.pathChanges += Number(workspace.counts?.total || 0);
|
|
371
|
+
}
|
|
372
|
+
return Object.freeze(counts);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function projectFrontierSummary(frontier = {}) {
|
|
376
|
+
return deepFreeze({
|
|
377
|
+
state: String(frontier.state || ''),
|
|
378
|
+
id: String(frontier.id || ''),
|
|
379
|
+
source: serializableSource(frontier.source || {}),
|
|
380
|
+
workspaces: Object.freeze((frontier.workspaces || []).map((workspace) => Object.freeze({
|
|
381
|
+
workspaceId: String(workspace.workspaceId || ''),
|
|
382
|
+
state: String(workspace.state || ''),
|
|
383
|
+
qualification: String(workspace.qualification || ''),
|
|
384
|
+
...(workspace.snapshot ? { snapshot: Object.freeze({ entryCount: Number(workspace.snapshot.entryCount || 0), totalBytes: Number(workspace.snapshot.totalBytes || 0), fingerprint: String(workspace.snapshot.fingerprint || '') }) } : {})
|
|
385
|
+
})))
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function projectWorkspaceSummary(workspace, maxPaths) {
|
|
390
|
+
if (workspace.delta) {
|
|
391
|
+
const added = workspace.delta.added || [];
|
|
392
|
+
const removed = workspace.delta.removed || [];
|
|
393
|
+
const changed = workspace.delta.byteChanged || [];
|
|
394
|
+
return deepFreeze({
|
|
395
|
+
workspaceId: workspace.workspaceId,
|
|
396
|
+
state: workspace.state,
|
|
397
|
+
delta: Object.freeze({
|
|
398
|
+
counts: workspace.delta.counts,
|
|
399
|
+
added: Object.freeze(added.slice(0, maxPaths).map((item) => item.path)),
|
|
400
|
+
removed: Object.freeze(removed.slice(0, maxPaths).map((item) => item.path)),
|
|
401
|
+
byteChanged: Object.freeze(changed.slice(0, maxPaths).map((item) => item.path)),
|
|
402
|
+
omitted: Math.max(0, added.length - maxPaths) + Math.max(0, removed.length - maxPaths) + Math.max(0, changed.length - maxPaths)
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (Array.isArray(workspace.paths)) {
|
|
407
|
+
return deepFreeze({
|
|
408
|
+
workspaceId: workspace.workspaceId,
|
|
409
|
+
state: workspace.state,
|
|
410
|
+
counts: workspace.counts || null,
|
|
411
|
+
paths: Object.freeze(workspace.paths.slice(0, maxPaths).map((item) => Object.freeze({ path: item.path, classification: item.classification, incomingChange: item.incomingChange, currentChange: item.currentChange }))),
|
|
412
|
+
pathsOmitted: Math.max(0, workspace.paths.length - maxPaths)
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return deepFreeze({ workspaceId: workspace.workspaceId, state: workspace.state });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function frontierReceipt(frontier) {
|
|
419
|
+
return deepFreeze({
|
|
420
|
+
schema: frontier.schema,
|
|
421
|
+
state: frontier.state,
|
|
422
|
+
id: frontier.id,
|
|
423
|
+
source: serializableSource(frontier.source || {}),
|
|
424
|
+
workspaces: Object.freeze((frontier.workspaces || []).map((workspace) => Object.freeze({
|
|
425
|
+
workspaceId: workspace.workspaceId,
|
|
426
|
+
state: workspace.state,
|
|
427
|
+
qualification: workspace.qualification,
|
|
428
|
+
...(workspace.state === QUALIFIED ? { snapshot: snapshotReceipt(workspace.snapshot) } : {}),
|
|
429
|
+
...(workspace.reason ? { reason: workspace.reason } : {})
|
|
430
|
+
})))
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function snapshotReceipt(snapshot) {
|
|
435
|
+
return Object.freeze({ state: snapshot.state, entryCount: snapshot.entryCount, totalBytes: snapshot.totalBytes, fingerprint: snapshot.fingerprint, fingerprintMethod: snapshot.fingerprintMethod, evidence: Object.freeze(serializableObject(snapshot.evidence || {})) });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function workspaceSide(workspace, options = {}) {
|
|
439
|
+
if (!workspace) return null;
|
|
440
|
+
return deepFreeze({
|
|
441
|
+
state: workspace.state,
|
|
442
|
+
qualification: workspace.qualification,
|
|
443
|
+
source: serializableSource(workspace.source || {}),
|
|
444
|
+
...(!options.opaque && workspace.state === QUALIFIED ? { snapshot: snapshotReceipt(workspace.snapshot) } : {}),
|
|
445
|
+
...(workspace.reason ? { reason: workspace.reason } : {})
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function ensureFrontier(frontier, label) {
|
|
450
|
+
if (frontier?.schema === PORTABLE_SOURCE_FRONTIER_SCHEMA_ID && Array.isArray(frontier.workspaces)) return createPortableSourceFrontier(frontier);
|
|
451
|
+
return createPortableSourceFrontier({
|
|
452
|
+
id: label,
|
|
453
|
+
state: 'qualification-error',
|
|
454
|
+
source: { kind: 'unavailable' },
|
|
455
|
+
workspaces: [],
|
|
456
|
+
findings: [portableFinding('error', 'portable.source-frontier.input.invalid', 'Comparison input is not a normalized source frontier.', { side: label })]
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function blockedShape(mode, code, message) {
|
|
461
|
+
const findings = Object.freeze([portableFinding('error', code, message)]);
|
|
462
|
+
return deepFreeze({ schema: PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID, mode, status: 'blocked', state: 'qualification-error', inputs: Object.freeze({}), workspaces: Object.freeze([]), counts: Object.freeze({ workspaces: 0 }), findings, findingSummary: summarizePortableFindings(findings), operationBoundary: comparisonBoundary(), boundary: 'Comparison did not run because required explicit frontier inputs were absent.' });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function snapshotFingerprint(entries) {
|
|
466
|
+
const stable = JSON.stringify(entries.map((entry) => [entry.path, entry.bytes, entry.sha256]));
|
|
467
|
+
return sha256Hex(utf8Bytes(stable));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function sameSnapshot(left, right) {
|
|
471
|
+
return Boolean(left && right && left.state === QUALIFIED && right.state === QUALIFIED && left.entryCount === right.entryCount && left.totalBytes === right.totalBytes && left.fingerprint && left.fingerprint === right.fingerprint);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function entryMap(snapshot) { return new Map((snapshot?.entries || []).map((entry) => [entry.path, entry])); }
|
|
475
|
+
function workspaceMap(frontier) { return new Map((frontier?.workspaces || []).map((workspace) => [workspace.workspaceId, workspace])); }
|
|
476
|
+
function sameEntry(left, right) { return Boolean(left && right && left.bytes === right.bytes && left.sha256 === right.sha256); }
|
|
477
|
+
function sameOptionalEntry(left, right) { return !left && !right ? true : Boolean(left && right && sameEntry(left, right)); }
|
|
478
|
+
function entryIdentity(entry) { return Object.freeze({ path: entry.path, bytes: entry.bytes, sha256: entry.sha256 }); }
|
|
479
|
+
function optionalEntryIdentity(entry) { return entry ? Object.freeze({ state: 'present', bytes: entry.bytes, sha256: entry.sha256 }) : Object.freeze({ state: 'absent' }); }
|
|
480
|
+
function changeKind(base, value) { if (sameOptionalEntry(base, value)) return 'unchanged'; if (!base && value) return 'added'; if (base && !value) return 'removed'; return 'byte-changed'; }
|
|
481
|
+
function emptyThreeWayCounts() { return Object.freeze({ incomingOnly: 0, currentOnly: 0, sameResultConcurrent: 0, conflictCandidate: 0, total: 0 }); }
|
|
482
|
+
|
|
483
|
+
function normalizePath(value = '') { return String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, ''); }
|
|
484
|
+
function unsafeRawPath(value = '') { const raw=String(value||'').trim(); return !raw || /^[\\/]/.test(raw) || /^[A-Za-z]:[\\/]/.test(raw) || raw.includes('\u0000') || /[\\/]$/.test(raw); }
|
|
485
|
+
function unsafePath(value = '') { return !value || value.includes('\u0000') || value.endsWith('/') || value.split('/').some((part) => !part || part === '.' || part === '..'); }
|
|
486
|
+
function positiveInteger(value, fallback) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }
|
|
487
|
+
function comparisonBoundary() { return Object.freeze({ readOnly: true, sourceMutation: false, remoteFetch: false, merge: false, semanticDiff: false, authorityInference: false, acceptanceInference: false }); }
|
|
488
|
+
function serializableSource(value = {}) { return Object.freeze(serializableObject(value)); }
|
|
489
|
+
function serializableObject(value) {
|
|
490
|
+
if (Array.isArray(value)) return value.map(serializableObject);
|
|
491
|
+
if (!value || typeof value !== 'object') return typeof value === 'undefined' || typeof value === 'function' ? null : value;
|
|
492
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return null;
|
|
493
|
+
const out = {};
|
|
494
|
+
for (const [key, child] of Object.entries(value)) {
|
|
495
|
+
if (typeof child === 'function' || typeof child === 'undefined' || ArrayBuffer.isView(child) || child instanceof ArrayBuffer) continue;
|
|
496
|
+
out[key] = serializableObject(child);
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
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; }
|
|
501
|
+
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); }
|
|
@@ -2,7 +2,7 @@ import { resolveLineage } from '../../../lineage/lineage.resolve.js';
|
|
|
2
2
|
import { normalizePortableInput } from '../input/portable.input.js';
|
|
3
3
|
import { projectPortableOperatingOverview } from '../overview/operatingOverview.js';
|
|
4
4
|
import { summarizePortableFindings } from '../findings.js';
|
|
5
|
-
import { directedLineageCone, isRoutedHandoffBundle, materializeQualifiedWorkspaceSnapshot, normalizeSelectors, projectBlockers, projectRelevantTopology, projectRequiredContext, relevantLineageIssues, resolveRequiredContextRecords, resolveSelectedRouteRecords } from './grounding.readiness.support.js';
|
|
5
|
+
import { directedLineageCone, isRoutedHandoffBundle, materializeQualifiedDetachedLineage, materializeQualifiedWorkspaceSnapshot, normalizeSelectors, projectBlockers, projectRelevantTopology, projectRequiredContext, relevantLineageIssues, resolveRequiredContextRecords, resolveSelectedRouteRecords } from './grounding.readiness.support.js';
|
|
6
6
|
import { groundPortableColdConsumer } from '../handoff/coldStartQualification.grounding.js';
|
|
7
7
|
import { createColdStartMaterialContext, projectGroundedContinuation } from '../handoff/coldStartQualification.materials.js';
|
|
8
8
|
import { auditHandoffPackageContextCarriage } from '../handoff/contextAudit.js';
|
|
@@ -51,10 +51,13 @@ export function projectPortableGroundingReadiness(input = {}, options = {}) {
|
|
|
51
51
|
const snapshot = materializeQualifiedWorkspaceSnapshot(bundle, contextAudit, {
|
|
52
52
|
includeLegacyTopics: Boolean(input.includeLegacyTopics || options.includeLegacyTopics)
|
|
53
53
|
});
|
|
54
|
+
const detachedLineage = materializeQualifiedDetachedLineage(bundle, contextAudit, {
|
|
55
|
+
includeLegacyTopics: Boolean(input.includeLegacyTopics || options.includeLegacyTopics)
|
|
56
|
+
});
|
|
54
57
|
const recoveryMaterial = acceptedRecoveryMaterial(input.recoveryAcceptance || input.recovery || input.recoveredMaterial || {});
|
|
55
58
|
const material = normalizePortableInput({
|
|
56
|
-
files: [...snapshot.files, ...recoveryMaterial.files],
|
|
57
|
-
findings: [...snapshot.findings, ...recoveryMaterial.findings]
|
|
59
|
+
files: [...snapshot.files, ...detachedLineage.files, ...recoveryMaterial.files],
|
|
60
|
+
findings: [...snapshot.findings, ...detachedLineage.findings, ...recoveryMaterial.findings]
|
|
58
61
|
});
|
|
59
62
|
return composeGroundingReadiness({
|
|
60
63
|
mode: 'routed-handoff-package',
|
|
@@ -69,6 +72,7 @@ export function projectPortableGroundingReadiness(input = {}, options = {}) {
|
|
|
69
72
|
...(grounding.findings || []),
|
|
70
73
|
...(contextAudit.findings || []),
|
|
71
74
|
...(snapshot.findings || []),
|
|
75
|
+
...(detachedLineage.findings || []),
|
|
72
76
|
...(material.findings || [])
|
|
73
77
|
]
|
|
74
78
|
});
|
|
@@ -143,7 +147,7 @@ export function composeGroundingReadiness({ mode = 'loaded-material', authority
|
|
|
143
147
|
if (!records.length) missing(missingEvidence, unresolved, 'no-artifact-records', 'No readable Tiinex artifact records were loaded for grounding.');
|
|
144
148
|
else known.push(evidence('loaded-artifact-records', 'known', `${records.length} record(s)`));
|
|
145
149
|
|
|
146
|
-
inferred.push(evidence('relevant-lineage-scope', 'bounded-inference', handoffMode ? 'directed declared-Parent cone around the exact selected Handoff route within complete carried Workspace snapshots' : 'all loaded records'));
|
|
150
|
+
inferred.push(evidence('relevant-lineage-scope', 'bounded-inference', handoffMode ? 'directed declared-Parent cone around the exact selected Handoff route within complete carried Workspace snapshots plus independently qualified exact detached Parent-boundary cache records' : 'all loaded records'));
|
|
147
151
|
inferred.push(evidence('lineage-leaf-role', 'bounded-inference', 'derived only from declared Parent edges in the shared resolver'));
|
|
148
152
|
|
|
149
153
|
if (handoffMode) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { packageFileBytes } from '../../../export/package.bytes.js';
|
|
1
|
+
import { packageFileBytes, sha256Hex } from '../../../export/package.bytes.js';
|
|
2
2
|
import { inspectStoredWorkspaceArchive } from '../handoff/workspaceByteProvider.js';
|
|
3
3
|
import { RECIPIENT_V2_READ_PATH } from '../handoff/recipientV2.topology.js';
|
|
4
4
|
import { decodeUtf8, findFile } from '../handoff/coldStartQualification.shared.js';
|
|
@@ -51,6 +51,68 @@ export function materializeQualifiedWorkspaceSnapshot(bundle, contextAudit, opti
|
|
|
51
51
|
return Object.freeze({ files: Object.freeze(files), findings: Object.freeze(findings) });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export function materializeQualifiedDetachedLineage(bundle, contextAudit, options = {}) {
|
|
55
|
+
const files = [];
|
|
56
|
+
const findings = [];
|
|
57
|
+
if (String(contextAudit?.status || '') !== 'ready' || String(contextAudit?.inspections?.parentBoundaryGrounding || '') !== 'qualified-package-v1') return Object.freeze({ files: Object.freeze(files), findings: Object.freeze(findings) });
|
|
58
|
+
for (const material of contextAudit?.lineageMaterializations || []) {
|
|
59
|
+
const workspaceId = String(material.workspaceId || '').trim();
|
|
60
|
+
const innerPath = String(material.workspaceRelativePath || '').replace(/^\/+/, '').replace(/\\/g, '/');
|
|
61
|
+
const archivePath = String(material.archivePackagePath || '').trim();
|
|
62
|
+
const archiveEntry = String(material.archiveEntry || '').trim();
|
|
63
|
+
if (!workspaceId || !innerPath || !archivePath || !archiveEntry) {
|
|
64
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.binding-incomplete', 'An independently qualified detached Parent-boundary projection lacks its exact Workspace/path/cache binding.', { workspaceId, innerPath, archivePath, archiveEntry, requirementId: String(material.requirementId || '') }));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!isGroundingArtifactPath(innerPath)) {
|
|
68
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.target-kind-invalid', 'Detached Parent-boundary grounding material must target one Tiinex trace/workspace artifact path.', { workspaceId, innerPath, requirementId: String(material.requirementId || '') }));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!options.includeLegacyTopics && /(?:^|\/)\.topics\/development(?:\/|$)/.test(innerPath)) continue;
|
|
72
|
+
const archiveFile = findFile(bundle, archivePath);
|
|
73
|
+
if (!archiveFile) {
|
|
74
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.cache-unavailable', 'Qualified detached Parent-boundary cache archive could not be resolved from the carrier.', { workspaceId, innerPath, archivePath }));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const archive = inspectStoredWorkspaceArchive(packageFileBytes(archiveFile), { ownedBytes: true });
|
|
78
|
+
if (archive.state !== 'qualified') {
|
|
79
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.cache-invalid', 'Detached Parent-boundary cache archive is not qualified readable material.', { workspaceId, innerPath, archivePath }));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const matches = (archive.entries || []).filter((entry) => String(entry.path || '') === archiveEntry);
|
|
83
|
+
if (matches.length !== 1) {
|
|
84
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.entry-unresolved', 'Detached Parent-boundary cache binding must resolve exactly one archive entry.', { workspaceId, innerPath, archivePath, archiveEntry, count: matches.length }));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const entry = matches[0];
|
|
88
|
+
const data = packageFileBytes({ data: entry.data });
|
|
89
|
+
const actualSha256 = sha256Hex(data);
|
|
90
|
+
if (Number(material.bytes || 0) !== data.byteLength || String(material.sha256 || '') !== actualSha256) {
|
|
91
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.entry-identity-mismatch', 'Detached Parent-boundary cache entry bytes diverge from the independently qualified package-v1 binding.', { workspaceId, innerPath, archiveEntry }));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const content = decodeUtf8(data);
|
|
95
|
+
if (!content) {
|
|
96
|
+
findings.push(finding('error', 'portable.grounding.detached-lineage.entry-unreadable', 'Detached Parent-boundary cache entry could not be decoded as grounding Markdown.', { workspaceId, innerPath, archiveEntry }));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
files.push(Object.freeze({
|
|
100
|
+
path: `${workspaceId}/${innerPath}`,
|
|
101
|
+
content,
|
|
102
|
+
size: data.byteLength,
|
|
103
|
+
sourceMode: 'portable-handoff-qualified-parent-boundary-cache',
|
|
104
|
+
locator: Object.freeze({
|
|
105
|
+
workspaceId,
|
|
106
|
+
workspaceRelativePath: innerPath,
|
|
107
|
+
archivePackagePath: archivePath,
|
|
108
|
+
archiveEntry,
|
|
109
|
+
qualification: String(material.qualification || '')
|
|
110
|
+
})
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze({ files: Object.freeze(files), findings: Object.freeze(findings) });
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
export function projectRequiredContext(requiredContext = [], selectors = []) {
|
|
55
117
|
const requested = normalizeSelectors(selectors);
|
|
56
118
|
const selected = (entry) => requested.includes('all') || requested.some((selector) => requiredContextMatches(entry, selector));
|