@tiinex/core 0.19.0 → 0.21.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.
Files changed (28) hide show
  1. package/package.json +7 -6
  2. package/src/schemas/creation.contracts.js +1 -1
  3. package/src/schemas/party/role/tiinex.party.role.v1.schema.js +5 -0
  4. package/src/schemas/party/role/tiinex.party.role.v1.schema.json +2 -2
  5. package/src/schemas/party/role/tiinex.party.role.v1.schema.md +313 -0
  6. package/src/schemas/party/role/tiinex.party.role.v1.schema.runtime.json +18 -6
  7. package/src/schemas/party/role/tiinex.party.role.v1.validate.js +36 -0
  8. package/src/schemas/schema.reference.js +29 -0
  9. package/src/tooling/portable/adapters/cli/cli.common-author.js +117 -6
  10. package/src/tooling/portable/adapters/cli/cli.common-output.js +19 -0
  11. package/src/tooling/portable/adapters/node/handoff.manufacture.js +26 -4
  12. package/src/tooling/portable/adapters/node/handoff.manufacture.packageParent.js +123 -0
  13. package/src/tooling/portable/audit/audit.capability.js +10 -4
  14. package/src/tooling/portable/grounding/grounding.capsule.js +26 -2
  15. package/src/tooling/portable/grounding/grounding.delegationArtifactAuthority.js +314 -0
  16. package/src/tooling/portable/grounding/grounding.delegationReadiness.js +241 -0
  17. package/src/tooling/portable/grounding/grounding.holderAssignmentModes.js +125 -0
  18. package/src/tooling/portable/grounding/grounding.holderBindingAuthorization.js +49 -50
  19. package/src/tooling/portable/grounding/grounding.readiness.js +19 -3
  20. package/src/tooling/portable/grounding/grounding.readiness.support.js +1 -1
  21. package/src/tooling/portable/handoff/carrierProjection.routeQualification.js +20 -4
  22. package/src/tooling/portable/handoff/coldStartQualification.grounding.js +67 -4
  23. package/src/tooling/portable/handoff/coldStartQualification.materials.js +84 -10
  24. package/src/tooling/portable/handoff/recipientV2.endpointRolePointers.js +1 -1
  25. package/src/tooling/portable/handoff/recipientV2.inspect.projection.js +5 -2
  26. package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +2 -2
  27. package/src/tooling/portable/handoff/recipientV2.topology.js +5 -3
  28. package/src/tooling/portable/handoff/recipientV2.topology.materials.js +0 -0
@@ -3,6 +3,7 @@ import { packageFileBytes, sha256Hex } from '../../../export/package.bytes.js';
3
3
  import { portableFinding } from '../findings.js';
4
4
  import { inspectStoredWorkspaceArchive } from './workspaceByteProvider.js';
5
5
  import { recipientV2FactsIndex } from './recipientV2.transportManifest.js';
6
+ import { deriveRecipientV2ArtifactFirstPhase1Facts } from './recipientV2.artifactFirst.materials.js';
6
7
  import { parseNamedDeclarationSection } from '../schema/named.declarations.js';
7
8
  import { resolveColdStartRolePointerMaterial } from './coldStartRolePointers.js';
8
9
  import {
@@ -58,7 +59,12 @@ export function collectPackageRoleMaterials(bundle = {}) {
58
59
  }
59
60
 
60
61
  export function resolveReferencedRoleMaterial(bundle = {}, handoff = {}, orientation = null, selectedRoute = null, findings = [], context = null) {
61
- const reference = String(handoff.toReference || '').trim();
62
+ return resolveReferencedEndpointRoleMaterial(bundle, handoff, orientation, selectedRoute, findings, context, 'to');
63
+ }
64
+
65
+ export function resolveReferencedEndpointRoleMaterial(bundle = {}, handoff = {}, orientation = null, selectedRoute = null, findings = [], context = null, endpointParty = 'to') {
66
+ const party = String(endpointParty || 'to').trim().toLowerCase() === 'from' ? 'from' : 'to';
67
+ const reference = String(party === 'from' ? handoff.fromReference : handoff.toReference || '').trim();
62
68
  if (!reference) return null;
63
69
  if (!isExternalReference(reference)) {
64
70
  const workspaceZip = findFile(bundle, String(handoff.packagePath || ''));
@@ -71,7 +77,7 @@ export function resolveReferencedRoleMaterial(bundle = {}, handoff = {}, orienta
71
77
  const markdown = decodeUtf8(matches[0].data || new Uint8Array());
72
78
  if (markdown) return Object.freeze({ path: `${workspaceZip.path}::${resolvedPath}`, markdown, explicit: false, exactReference: reference });
73
79
  }
74
- if (matches.length > 1) findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff To Reference resolves to multiple entries in the selected Workspace archive.', { reference, resolvedPath }));
80
+ if (matches.length > 1) findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff endpoint Reference resolves to multiple entries in the selected Workspace archive.', { reference, resolvedPath }));
75
81
  }
76
82
  }
77
83
  }
@@ -85,14 +91,14 @@ export function resolveReferencedRoleMaterial(bundle = {}, handoff = {}, orienta
85
91
  : projectedFacts
86
92
  ? { role: 'endpoint-role', ...projectedFacts }
87
93
  : {};
88
- if (facts.role !== 'endpoint-role' || String(facts.endpointParty || '').toLowerCase() !== 'to') continue;
94
+ if (facts.role !== 'endpoint-role' || String(facts.endpointParty || '').toLowerCase() !== party) continue;
89
95
  if (facts.referenceTarget && String(facts.referenceTarget) !== reference) continue;
90
96
  const material = resolveColdStartRolePointerMaterial(bundle, facts, findings, String(pointerPath || ''), 'endpoint-role');
91
97
  if (material) endpointPointerMatches.push(Object.freeze({ ...material, exactReference: reference }));
92
98
  }
93
99
  if (endpointPointerMatches.length === 1) return endpointPointerMatches[0];
94
100
  if (endpointPointerMatches.length > 1) {
95
- findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff To Reference resolves through multiple qualified endpoint Role Pointers.', { reference, count: endpointPointerMatches.length }));
101
+ findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff endpoint Reference resolves through multiple qualified endpoint Role Pointers.', { reference, count: endpointPointerMatches.length }));
96
102
  return null;
97
103
  }
98
104
  const cacheMatches = [];
@@ -114,7 +120,7 @@ export function resolveReferencedRoleMaterial(bundle = {}, handoff = {}, orienta
114
120
  }
115
121
  }
116
122
  if (cacheMatches.length === 1) return cacheMatches[0];
117
- if (cacheMatches.length > 1) findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff To Reference resolves to multiple cached exact byte carriers.', { reference, count: cacheMatches.length }));
123
+ if (cacheMatches.length > 1) findings.push(portableFinding('error', 'portable.cold-start.role.reference-material.ambiguous', 'Handoff endpoint Reference resolves to multiple cached exact byte carriers.', { reference, count: cacheMatches.length }));
118
124
  return null;
119
125
  }
120
126
 
@@ -172,9 +178,16 @@ export function parseRoleMaterial(entry) {
172
178
  label,
173
179
  roleKind: sectionField(roleSection, 'Role Kind'),
174
180
  boundary: Object.freeze({ inScope: sectionField(boundarySection, 'In Scope'), outOfScope: sectionField(boundarySection, 'Out Of Scope'), context: sectionField(boundarySection, 'Context') }),
175
- authorityBoundary: Object.freeze({ mayDo: sectionField(authoritySection, 'May Do'), doesNotAuthorize: sectionField(authoritySection, 'Does Not Authorize'), reviewBoundary: sectionField(authoritySection, 'Review Boundary') }),
181
+ authorityBoundary: Object.freeze({
182
+ mayDo: sectionField(authoritySection, 'May Do'),
183
+ doesNotAuthorize: sectionField(authoritySection, 'Does Not Authorize'),
184
+ requiredInstrument: sectionField(authoritySection, 'Required Instrument'),
185
+ delegation: sectionField(authoritySection, 'Delegation'),
186
+ reviewBoundary: sectionField(authoritySection, 'Review Boundary')
187
+ }),
176
188
  holderRelationship: Object.freeze({
177
189
  holderState: sectionField(holderSection, 'Holder State'),
190
+ assignmentModes: sectionField(holderSection, 'Assignment Modes'),
178
191
  currentHolder: sectionField(holderSection, 'Current Holder'),
179
192
  possibleHolder: sectionField(holderSection, 'Possible Holder'),
180
193
  unknownHolder: sectionField(holderSection, 'Unknown Holder'),
@@ -262,7 +275,7 @@ function hydrateRequiredContextEntry(bundle = {}, entry = {}, context = null) {
262
275
  })
263
276
  };
264
277
  if (base.state !== 'qualified') return Object.freeze({ ...base, contentState: 'unavailable', content: '' });
265
- const hydrated = resolveQualifiedMaterialBytes(bundle, resolution, context);
278
+ const hydrated = resolveQualifiedMaterialBytes(bundle, resolution, context, base);
266
279
  if (!hydrated.bytes) return Object.freeze({ ...base, state: 'unresolved', contentState: 'unavailable', content: '' });
267
280
  const actualSha256 = sha256Hex(hydrated.bytes);
268
281
  const identityQualified = (!base.bytes || hydrated.bytes.byteLength === base.bytes) && (!base.sha256 || actualSha256 === base.sha256);
@@ -270,14 +283,21 @@ function hydrateRequiredContextEntry(bundle = {}, entry = {}, context = null) {
270
283
  return Object.freeze({
271
284
  ...base,
272
285
  state: identityQualified ? 'qualified' : 'identity-mismatch',
286
+ workspaceId: String(hydrated.workspaceId || base.workspaceId || ''),
287
+ archivePackagePath: String(hydrated.archivePackagePath || base.archivePackagePath || ''),
288
+ innerPath: String(hydrated.innerPath || base.innerPath || ''),
289
+ packagePath: String(hydrated.packagePath || base.packagePath || ''),
290
+ providerMode: String(hydrated.providerMode || base.providerMode || ''),
291
+ kind: String(hydrated.kind || base.kind || ''),
273
292
  actualBytes: hydrated.bytes.byteLength,
274
293
  actualSha256,
294
+ provenance: Object.freeze({ ...(base.provenance || {}), providerMode: String(hydrated.providerMode || base.providerMode || ''), resolutionKind: String(hydrated.kind || base.kind || '') }),
275
295
  contentState: text ? 'hydrated-text' : identityQualified ? 'qualified-locator-only' : 'unavailable',
276
296
  content: text
277
297
  });
278
298
  }
279
299
 
280
- function resolveQualifiedMaterialBytes(bundle = {}, resolution = {}, context = null) {
300
+ function resolveQualifiedMaterialBytes(bundle = {}, resolution = {}, context = null, requirement = {}) {
281
301
  const kind = String(resolution.kind || '');
282
302
  if (kind === 'workspace-archive-entry' || kind === 'workspace-cache-entry') {
283
303
  const archivePath = String(resolution.archivePackagePath || resolution.packagePath || '');
@@ -293,17 +313,64 @@ function resolveQualifiedMaterialBytes(bundle = {}, resolution = {}, context = n
293
313
  }
294
314
  const packagePath = String(resolution.packagePath || '');
295
315
  const file = packagePath ? findFile(bundle, packagePath) : null;
296
- return Object.freeze({ bytes: file ? packageFileBytes(file) : null });
316
+ if (file) return Object.freeze({ bytes: packageFileBytes(file), packagePath });
317
+ if (kind === 'materialized-required-material') {
318
+ const requirementId = String(requirement.requirementId || '');
319
+ const referenceTarget = String(requirement.referenceTarget || '');
320
+ const matches = [];
321
+ const factsIndex = recipientFactsIndexForColdStart(bundle, context).map;
322
+ const visibleFactsIndex = deriveRecipientV2ArtifactFirstPhase1Facts(bundle.files || []);
323
+ for (const candidateFile of bundle.files || []) {
324
+ const candidatePath = String(candidateFile.path || '');
325
+ const facts = factsIndex.get(candidatePath) || visibleFactsIndex.get(candidatePath) || null;
326
+ if (facts?.role !== 'workspace-dependency-cache' && !/dependency cache/i.test(String(facts?.role || ''))) continue;
327
+ for (const material of facts.materials || []) {
328
+ if (requirementId && String(material.requirementId || '') !== requirementId) continue;
329
+ if (referenceTarget && String(material.referenceTarget || '') !== referenceTarget) continue;
330
+ const archivePath = String(facts.archivePath || '');
331
+ const archiveFile = findFile(bundle, archivePath);
332
+ if (!archiveFile) continue;
333
+ const archive = inspectWorkspaceArchiveForColdStart(archiveFile, context);
334
+ if (archive.state !== 'qualified') continue;
335
+ const entries = (archive.entries || []).filter((entry) => String(entry.path || '') === String(material.archiveEntry || ''));
336
+ if (entries.length !== 1) continue;
337
+ const entry = entries[0];
338
+ const data = packageFileBytes({ data: entry.data });
339
+ const actualSha = sha256Hex(data);
340
+ if (Number(material.bytes || 0) && Number(material.bytes || 0) !== data.byteLength) continue;
341
+ if (String(material.sha256 || '') && String(material.sha256 || '') !== actualSha) continue;
342
+ if (Number(resolution.bytes || 0) && Number(resolution.bytes || 0) !== data.byteLength) continue;
343
+ if (String(resolution.sha256 || '') && String(resolution.sha256 || '') !== actualSha) continue;
344
+ matches.push(Object.freeze({
345
+ bytes: data, providerMode: 'cache', kind: 'workspace-cache-entry',
346
+ workspaceId: String(material.targetWorkspaceId || material.sourceWorkspaceId || facts.workspaceId || ''),
347
+ innerPath: String(material.targetPath || material.originalPath || ''),
348
+ archivePackagePath: archivePath, packagePath: archivePath
349
+ }));
350
+ }
351
+ }
352
+ if (matches.length === 1) return matches[0];
353
+ }
354
+ return Object.freeze({ bytes: null });
297
355
  }
298
356
 
299
357
  export function parseHandoffGrounding(markdown, route) {
300
358
  const parties = sectionText(markdown, 'Handoff Parties');
301
359
  const transferSection = parseNamedDeclarationSection(markdown, '## Transfers');
360
+ const retainedSection = parseNamedDeclarationSection(markdown, '## Retained Responsibilities');
302
361
  const completion = sectionText(markdown, 'Completion Expectation');
303
362
  const transfers = Object.freeze((transferSection.entries || []).filter((entry) => String(entry.name || '').trim().toLowerCase() !== 'none').map((entry) => Object.freeze({
304
363
  id: String(entry.name || ''),
305
364
  transferKind: String(entry.fields?.['Transfer Kind'] || ''),
306
365
  description: String(entry.fields?.Description || ''),
366
+ controllingArtifact: String(entry.fields?.['Controlling Artifact'] || ''),
367
+ controllingArtifactTarget: markdownReferenceTarget(entry.fields?.['Controlling Artifact'] || ''),
368
+ boundary: String(entry.fields?.Boundary || '')
369
+ })));
370
+ const retainedResponsibilities = Object.freeze((retainedSection.entries || []).filter((entry) => String(entry.name || '').trim().toLowerCase() !== 'none').map((entry) => Object.freeze({
371
+ id: String(entry.name || ''),
372
+ retainedBy: String(entry.fields?.['Retained By'] || ''),
373
+ responsibility: String(entry.fields?.Responsibility || ''),
307
374
  boundary: String(entry.fields?.Boundary || '')
308
375
  })));
309
376
  return deepFreeze({
@@ -316,6 +383,7 @@ export function parseHandoffGrounding(markdown, route) {
316
383
  fromReference: sectionReferenceTarget(parties, 'From Reference'),
317
384
  toReference: sectionReferenceTarget(parties, 'To Reference'),
318
385
  transfers,
386
+ retainedResponsibilities,
319
387
  completionExpectation: Object.freeze({
320
388
  signalKind: sectionField(completion, 'Signal Kind'),
321
389
  signalMeaning: sectionField(completion, 'Signal Meaning'),
@@ -330,8 +398,14 @@ export function parseHandoffGrounding(markdown, route) {
330
398
  });
331
399
  }
332
400
 
401
+ function markdownReferenceTarget(value = '') {
402
+ const text = String(value || '').trim();
403
+ const match = text.match(/\[[^\]]*\]\(([^)]+)\)/);
404
+ return match ? String(match[1] || '').trim() : '';
405
+ }
406
+
333
407
  export function emptyHandoffGrounding() {
334
- return deepFreeze({ schemaId: '', purpose: '', from: '', fromKind: '', fromReference: '', to: '', toKind: '', toReference: '', transfers: Object.freeze([]), completionExpectation: Object.freeze({ signalKind: '', signalMeaning: '', returnTo: '' }), routeId: '', workspaceId: '', workspaceRelativePath: '', packagePath: '', sha256: '', boundary: 'No Handoff material supplied.' });
408
+ return deepFreeze({ schemaId: '', purpose: '', from: '', fromKind: '', fromReference: '', to: '', toKind: '', toReference: '', transfers: Object.freeze([]), retainedResponsibilities: Object.freeze([]), completionExpectation: Object.freeze({ signalKind: '', signalMeaning: '', returnTo: '' }), routeId: '', workspaceId: '', workspaceRelativePath: '', packagePath: '', sha256: '', boundary: 'No Handoff material supplied.' });
335
409
  }
336
410
 
337
411
  export function resolveGroundingRouteMarkdown(bundle = {}, selectedRoute = {}, findings = [], context = null) {
@@ -136,7 +136,7 @@ export function buildParticipantRolePointerChain(input = {}) {
136
136
  prose: 'This Pointer is recipient discovery/grounding only. It does not declare semantic participation, change Handoff From/To, prove a human holder, or create Role authority; participation meaning remains owned by authoritative Handoff/Relation/context authority.',
137
137
  currentRead: [
138
138
  { label: 'Route Id', value: `\`${String(input.route?.id || '')}\`` },
139
- { label: 'Grounding Requirement Id', value: `\`${String(requirement.id || '')}\`` },
139
+ { label: 'Participant Requirement Id', value: `\`${String(requirement.id || '')}\`` },
140
140
  ...(roleFacts.roleLabelHint ? [{ label: 'Role Label Hint', value: roleFacts.roleLabelHint }] : []),
141
141
  { label: 'Role Reference', value: roleFacts.referenceTarget ? `\`${roleFacts.referenceTarget}\`` : 'exact carried target' },
142
142
  { label: 'Target Carrier Kind', value: target.carrierKind },
@@ -38,14 +38,17 @@ export function projectRecipientV2EndpointRoles(pointers = []) {
38
38
  export function projectRecipientV2ParticipantRoles(pointers = []) {
39
39
  return Object.freeze(pointers.map((item) => Object.freeze({
40
40
  pointerPath: item.path,
41
- routeId: String(item.facts?.routeId || qualified?.id || ''),
42
- workspaceId: String(item.facts?.workspaceId || ''),
43
41
  routeId: String(item.facts?.routeId || ''),
42
+ workspaceId: String(item.facts?.workspaceId || ''),
43
+ requirementId: String(item.facts?.participantRequirementId || ''),
44
44
  roleLabelHint: String(item.facts?.roleLabelHint || ''),
45
45
  referenceTarget: String(item.facts?.referenceTarget || ''),
46
46
  targetCarrierKind: String(item.facts?.targetCarrierKind || ''),
47
47
  targetWorkspaceId: String(item.facts?.targetWorkspaceId || ''),
48
+ archivePath: String(item.facts?.archivePath || ''),
48
49
  targetInnerPath: String(item.facts?.targetInnerPath || item.facts?.targetArchiveEntry || ''),
50
+ targetArchiveEntry: String(item.facts?.targetArchiveEntry || ''),
51
+ targetBytes: Number(item.facts?.targetBytes || 0),
49
52
  targetSha256: String(item.facts?.targetSha256 || '')
50
53
  })));
51
54
  }
@@ -9,7 +9,7 @@ import { RECIPIENT_V2_ROUTE_SELECTION_AUTHORITY, RECIPIENT_V2_SIBLING_ROUTE_INFE
9
9
  import { recipientV2TransportFacts } from './recipientV2.transportManifest.js';
10
10
  import { buildRecipientV2BootstrapCarrier, buildRecipientV2WorkspaceCarriers, recipientV2ParentAuthority } from './recipientV2.topology.workspaces.js';
11
11
  import { buildEndpointRolePointerChain, buildParticipantRolePointerChain } from './recipientV2.endpointRolePointers.js';
12
- import { bindingForWorkspace, boundedWorkspaceClaimsDetachedRecovery, detachedMaterial, duplicates, finding, roleMaterialTarget, routeClaimsDetachedMaterial, safeToken, uniqueFileIndex } from './recipientV2.topology.materials.js';
12
+ import { bindingForWorkspace, boundedWorkspaceClaimsDetachedRecovery, coalesceDetachedCacheMaterials, detachedMaterial, duplicates, finding, roleMaterialTarget, routeClaimsDetachedMaterial, safeToken, uniqueFileIndex } from './recipientV2.topology.materials.js';
13
13
  import { RECIPIENT_V2_PACKAGE_V1_FORMAT_ID, RECIPIENT_V2_PACKAGE_V1_ROOT_PATH, RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID, RECIPIENT_V2_PACKAGE_V1_SCHEMA_TARGET } from './recipientV2.packageV1.constants.js';
14
14
  import { renderHandoffPackageV1 } from './recipientV2.packageV1.contract.js';
15
15
  import { inspectRecipientFacingV2PackageV1 } from './recipientV2.packageV1.inspect.js';
@@ -136,7 +136,7 @@ function buildRecipientFacingV2PackageV1Prepared(input = {}, sealedByWorkspaceId
136
136
  const workspace = workspaceById.get(plan.workspaceId);
137
137
  if (!workspace) continue;
138
138
  const binding = bindingForWorkspace(descriptor, workspace.workspaceId);
139
- const materials = detached.filter((item) => (workspace.workspaceId === String(route.workspaceId || '') && routeClaimsDetachedMaterial(route, item)) || boundedWorkspaceClaimsDetachedRecovery(binding, item));
139
+ const materials = coalesceDetachedCacheMaterials(detached.filter((item) => (workspace.workspaceId === String(route.workspaceId || '') && routeClaimsDetachedMaterial(route, item)) || boundedWorkspaceClaimsDetachedRecovery(binding, item)));
140
140
  if (!materials.length) continue;
141
141
  const artifactPath = `${plan.prefix}-1-cache.trace.md`;
142
142
  const archivePath = `${plan.prefix}-1-cache.zip`;
@@ -16,6 +16,8 @@ import { buildEndpointRolePointerChain, buildParticipantRolePointerChain } from
16
16
  import {
17
17
  bindingForWorkspace,
18
18
  boundedWorkspaceClaimsDetachedRecovery,
19
+ coalesceDetachedCacheMaterials,
20
+ detachedCacheRepresentationKey,
19
21
  deepFreeze,
20
22
  detachedMaterial,
21
23
  duplicates,
@@ -144,7 +146,7 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
144
146
  if (!workspace) continue;
145
147
  const workspaceRoutes = routePlans.filter((plan) => plan.workspace.workspaceId === workspace.workspaceId);
146
148
  const binding = bindingForWorkspace(descriptor, workspace.workspaceId);
147
- const materials = detached.filter((item) => workspaceRoutes.some((plan) => routeClaimsDetachedMaterial(plan.route, item)) || boundedWorkspaceClaimsDetachedRecovery(binding, item));
149
+ const materials = coalesceDetachedCacheMaterials(detached.filter((item) => workspaceRoutes.some((plan) => routeClaimsDetachedMaterial(plan.route, item)) || boundedWorkspaceClaimsDetachedRecovery(binding, item)));
148
150
  if (!materials.length) continue;
149
151
  const artifactPath = `001-${workspacePlan.ordinal}-1-cache.trace.md`;
150
152
  const archivePath = `001-${workspacePlan.ordinal}-1-cache.zip`;
@@ -166,9 +168,9 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
166
168
  }
167
169
 
168
170
  const claimedDetached = new Set();
169
- for (const cache of topology.caches) for (const material of cache.materials || []) claimedDetached.add(`${material.requirementId}\u0000${material.referenceTarget}\u0000${material.sha256}`);
171
+ for (const cache of topology.caches) for (const material of cache.materials || []) claimedDetached.add(detachedCacheRepresentationKey(material));
170
172
  for (const item of detached) {
171
- const key = `${item.requirementId}\u0000${item.referenceTarget}\u0000${item.sha256}`;
173
+ const key = detachedCacheRepresentationKey(item);
172
174
  if (!claimedDetached.has(key)) findings.push(finding('error', 'portable.handoff-v2-surface.cache.material-unowned', 'Detached dependency bytes are not claimed by any Workspace-scoped Handoff route cache.', { requirementId: item.requirementId || '', referenceTarget: item.referenceTarget || '' }));
173
175
  }
174
176