@tiinex/core 0.11.0 → 0.13.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 (36) hide show
  1. package/package.json +6 -5
  2. package/src/audit/audit.run.js +1 -1
  3. package/src/public/index.js +18 -0
  4. package/src/public/node.js +1 -0
  5. package/src/schemas/creation.contracts.js +1 -1
  6. package/src/schemas/creation.schemaReferences.js +28 -8
  7. package/src/schemas/schema.reference.js +30 -4
  8. package/src/schemas/tiinex.root.v1.schema.json +2 -1
  9. package/src/tooling/portable/adapters/cli/cli.command-input.js +5 -0
  10. package/src/tooling/portable/adapters/cli/cli.common-output.js +23 -0
  11. package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +5 -1
  12. package/src/tooling/portable/adapters/cli/cli.help.js +15 -2
  13. package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
  14. package/src/tooling/portable/adapters/cli/cli.source-frontier-comparison.js +6 -6
  15. package/src/tooling/portable/adapters/cli/cli.source-frontier-reconciliation.js +18 -0
  16. package/src/tooling/portable/adapters/node/handoff.manufacture.bootstrap.js +37 -1
  17. package/src/tooling/portable/adapters/node/handoff.manufacture.enumeration.js +27 -8
  18. package/src/tooling/portable/adapters/node/handoff.manufacture.js +27 -0
  19. package/src/tooling/portable/adapters/node/handoff.manufacture.runtimeSource.js +82 -0
  20. package/src/tooling/portable/adapters/node/sourceFrontierComparison.js +2 -1
  21. package/src/tooling/portable/adapters/node/sourceFrontierReconciliationProof.js +23 -0
  22. package/src/tooling/portable/adapters/node/workspaceCarrier.manufacture.js +8 -1
  23. package/src/tooling/portable/audit/audit.capability.js +2 -1
  24. package/src/tooling/portable/comparison/sourceFrontierComparison.js +54 -4
  25. package/src/tooling/portable/comparison/sourceFrontierReconciliationProof.js +569 -0
  26. package/src/tooling/portable/draft/draft.create.js +3 -0
  27. package/src/tooling/portable/draft/draft.operations.js +1 -1
  28. package/src/tooling/portable/editor/editor.assistance.js +2 -25
  29. package/src/tooling/portable/handoff/manufacture.js +15 -3
  30. package/src/tooling/portable/handoff/pointerEntrypoint.js +4 -1
  31. package/src/tooling/portable/handoff/schemaReferencePreflight.js +26 -0
  32. package/src/tooling/portable/handoff/transportEnvelopeV1.js +5 -0
  33. package/src/tooling/portable/index.js +1 -0
  34. package/src/tooling/portable/operation.catalog.js +8 -0
  35. package/src/tooling/portable/source/sourceEligibility.js +108 -0
  36. package/src/validation/validateArtifact.js +62 -6
@@ -2,10 +2,13 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { inferWorkspaceTitle, normalizeAdditionalWorkspaceDescriptors, normalizeTransportRoute, safeWorkspaceToken } from './handoff.manufacture.multiRoot.js';
4
4
  import { buildToolingBootstrapTransportFiles, PORTABLE_TOOLING_BOOTSTRAP_MANIFEST_SCHEMA_ID } from './handoff.manufacture.bootstrap.js';
5
+ import { qualifyToolingRuntimeSourceAlignment } from './handoff.manufacture.runtimeSource.js';
5
6
  import { normalizeHandoffCarrierLineage } from '../../handoff/carrierLineage.js';
6
7
  import { normalizeHandoffCarrierProfile } from '../../handoff/carrierProfile.js';
7
8
  import { enumerateNodeWorkspace, PORTABLE_NODE_WORKSPACE_ENUMERATION_SCHEMA_ID } from './handoff.manufacture.enumeration.js';
8
9
  import { preparePackageParentWorkspaceReuse } from './handoff.manufacture.packageParent.js';
10
+ import { qualifyPortableSourceReconciliationProofForManufacture } from '../../comparison/sourceFrontierReconciliationProof.js';
11
+ import { qualifyPortableManufactureSchemaReferenceCandidate } from '../../handoff/schemaReferencePreflight.js';
9
12
  import {
10
13
  assertInside,
11
14
  expandPointerDependencyClosure,
@@ -47,6 +50,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
47
50
  workspaceTitle: requestedWorkspaceTitle,
48
51
  sourceMetadata: input.workspaceSource || input.sourceMetadata || {},
49
52
  excludeDirectories: input.excludeDirectories || options.excludeDirectories,
53
+ excludeRelativePaths: input.excludeRelativePaths || options.excludeRelativePaths,
50
54
  maxFiles: input.maxFiles || options.maxFiles
51
55
  });
52
56
  const additionalWorkspaceDescriptors = normalizeAdditionalWorkspaceDescriptors(input.additionalWorkspaces || input.workspaceRoots || input.workspaceDescriptors || []);
@@ -78,6 +82,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
78
82
  workspaceTitle: requestedTitle,
79
83
  sourceMetadata: descriptor.source || descriptor.sourceMetadata || {},
80
84
  excludeDirectories: descriptor.excludeDirectories || input.excludeDirectories || options.excludeDirectories,
85
+ excludeRelativePaths: descriptor.excludeRelativePaths || input.excludeRelativePaths || options.excludeRelativePaths,
81
86
  maxFiles: descriptor.maxFiles || input.maxFiles || options.maxFiles
82
87
  });
83
88
  if (enumerated.status !== 'qualified-complete') throw new Error(`portable.handoff-manufacture.workspace-enumeration.${id}.${enumerated.status}`);
@@ -96,6 +101,8 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
96
101
  markdown: handoffMarkdown
97
102
  });
98
103
 
104
+ const schemaReferencePreflight = qualifyPortableManufactureSchemaReferenceCandidate(handoff);
105
+
99
106
  if (enumeration.status !== 'qualified-complete') throw new Error(`portable.handoff-manufacture.workspace-enumeration.${enumeration.status}`);
100
107
  const workspaceTitle = requestedWorkspaceTitle || inferWorkspaceTitle(enumeration) || workspaceId;
101
108
  const primaryMaterialization = Object.freeze({ ...enumeration.materialization, title: workspaceTitle });
@@ -137,6 +144,13 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
137
144
  if (targets.length !== 1) throw new Error(`portable.handoff-manufacture.workspace-scope.target-${targets.length ? 'ambiguous' : 'required'}:${materialization.id}`);
138
145
  workspaceMaterializations[index] = projectBoundedWorkspaceMaterialization(materialization, scope, targets[0].path);
139
146
  }
147
+ const hasReconciliationProof = Boolean(input.reconciliationProof && typeof input.reconciliationProof === 'object' && Object.keys(input.reconciliationProof).length);
148
+ const reconciliationProofQualification = qualifyPortableSourceReconciliationProofForManufacture({
149
+ proof: input.reconciliationProof || null,
150
+ requireProof: input.requireReconciliationProof === true,
151
+ workspaceMaterializations,
152
+ requiredWorkspaceIds: hasReconciliationProof || input.requireReconciliationProof === true ? [workspaceId] : []
153
+ });
140
154
 
141
155
  const routeSpecs = transportRoutes.length ? transportRoutes : Object.freeze([{ workspaceId, path: handoffPath }]);
142
156
  let requirements = await projectManufacturingRequirements({ handoff, workspaceId, handoffPath, routeSpecs, workspaceRuntimeById });
@@ -153,6 +167,14 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
153
167
  const toolingBootstrapResult = await toolingBootstrapPromise;
154
168
  if (toolingBootstrapResult.error) throw toolingBootstrapResult.error;
155
169
  const toolingBootstrap = toolingBootstrapResult.value;
170
+ const runtimeSourceAlignment = await qualifyToolingRuntimeSourceAlignment({
171
+ runtimeIdentity: toolingBootstrap.runtimeIdentity,
172
+ localWorkspaces: [
173
+ Object.freeze({ id: workspaceId, root: workspaceRoot, materialization: primaryMaterialization }),
174
+ ...additionalEnumerations.map(({ id, root, enumerated }) => Object.freeze({ id, root, materialization: enumerated.materialization }))
175
+ ],
176
+ maxFiles: input.bootstrapMaxFiles || options.bootstrapMaxFiles
177
+ });
156
178
  const orientationBootstrap = input.transportBootstrapContent
157
179
  ? Object.freeze({ present: true, path: String(input.transportBootstrapPath || 'tiinex.package/bootstrap.md'), content: String(input.transportBootstrapContent), mediaType: 'text/markdown' })
158
180
  : Object.freeze({ present: false });
@@ -171,10 +193,15 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
171
193
  carrierLineage: normalizeHandoffCarrierLineage(input.carrierLineage || null),
172
194
  carrierProfile: normalizeHandoffCarrierProfile(input.carrierProfile || null),
173
195
  toolingBootstrap: toolingBootstrap.summary,
196
+ reconciliationProofQualification,
197
+ schemaReferencePreflight,
174
198
  manufacturingEvidence: Object.freeze({
175
199
  enumeration: enumeration.evidence,
176
200
  workspaceEnumerations: Object.freeze(workspaceEnumerations),
177
201
  toolingBootstrap: toolingBootstrap.summary,
202
+ runtimeSourceAlignment,
203
+ reconciliationProof: reconciliationProofQualification,
204
+ schemaReferencePreflight,
178
205
  packageParentWorkspaceReuse: Object.freeze({
179
206
  state: String(packageParentReuse.state || ''),
180
207
  providerState: String(packageParentReuse.providerState || ''),
@@ -0,0 +1,82 @@
1
+ import { buildToolingBootstrapRuntimeIdentity } from './handoff.manufacture.bootstrap.js';
2
+
3
+ export const PORTABLE_TOOLING_RUNTIME_SOURCE_ALIGNMENT_SCHEMA_ID = 'tiinex.portable.tooling-runtime-source-alignment.v1';
4
+ const CORE_PACKAGE_NAME = '@tiinex/core';
5
+
6
+ export async function qualifyToolingRuntimeSourceAlignment(input = {}) {
7
+ const runtimeIdentity = normalizeIdentity(input.runtimeIdentity);
8
+ if (!runtimeIdentity.sourceRepresentationSha256) throw new Error('portable.tooling-bootstrap.runtime-source.runtime-identity-required');
9
+ const candidates = [];
10
+ for (const item of input.localWorkspaces || input.workspaces || []) {
11
+ const root = String(item?.root || '').trim();
12
+ if (!root) continue;
13
+ const materialization = item?.materialization || item?.enumeration?.materialization || null;
14
+ const packageIdentity = packageIdentityFromMaterialization(materialization);
15
+ if (packageIdentity.name !== CORE_PACKAGE_NAME) continue;
16
+ candidates.push(Object.freeze({
17
+ workspaceId: String(item?.id || materialization?.id || '').trim(),
18
+ root,
19
+ packageIdentity
20
+ }));
21
+ }
22
+ if (!candidates.length) return Object.freeze({
23
+ schema: PORTABLE_TOOLING_RUNTIME_SOURCE_ALIGNMENT_SCHEMA_ID,
24
+ state: 'not-observed',
25
+ coreWorkspaceCount: 0,
26
+ runtime: runtimeIdentity,
27
+ source: null,
28
+ boundary: 'No local Workspace with package identity @tiinex/core was selected, so exact runtime/source equality cannot be asserted from carried source.'
29
+ });
30
+ if (candidates.length !== 1) throw new Error(`portable.tooling-bootstrap.runtime-source.core-workspace-ambiguous:${candidates.map((item) => item.workspaceId || 'unlabeled').join(',')}`);
31
+
32
+ const candidate = candidates[0];
33
+ const sourceIdentity = normalizeIdentity(await buildToolingBootstrapRuntimeIdentity({
34
+ runtimeRoot: candidate.root,
35
+ maxFiles: input.maxFiles
36
+ }));
37
+ const evidence = Object.freeze({
38
+ schema: PORTABLE_TOOLING_RUNTIME_SOURCE_ALIGNMENT_SCHEMA_ID,
39
+ state: sourceIdentity.sourceRepresentationSha256 === runtimeIdentity.sourceRepresentationSha256 ? 'qualified-exact-match' : 'mismatch',
40
+ coreWorkspaceCount: 1,
41
+ workspaceId: candidate.workspaceId,
42
+ runtime: runtimeIdentity,
43
+ source: sourceIdentity,
44
+ boundary: 'Exact Tooling runtime source/data equality is required when a local @tiinex/core Workspace is carried. Release-normalized package.json metadata may differ, but matching package names or versions alone are insufficient.'
45
+ });
46
+ if (evidence.state !== 'qualified-exact-match') {
47
+ throw new Error([
48
+ 'portable.tooling-bootstrap.runtime-source.mismatch',
49
+ `workspace=${candidate.workspaceId || 'unlabeled'}`,
50
+ `runtimeVersion=${runtimeIdentity.packageVersion || 'unknown'}`,
51
+ `sourceVersion=${sourceIdentity.packageVersion || candidate.packageIdentity.version || 'unknown'}`,
52
+ `runtimeSource=${runtimeIdentity.sourceRepresentationSha256}`,
53
+ `sourceSource=${sourceIdentity.sourceRepresentationSha256}`
54
+ ].join(':'));
55
+ }
56
+ return evidence;
57
+ }
58
+
59
+ function packageIdentityFromMaterialization(materialization) {
60
+ const entry = (materialization?.entries || []).find((item) => String(item?.path || '') === 'package.json');
61
+ if (!entry?.data) return Object.freeze({ name: '', version: '' });
62
+ try {
63
+ const parsed = JSON.parse(new TextDecoder().decode(entry.data));
64
+ return Object.freeze({ name: String(parsed?.name || '').trim(), version: String(parsed?.version || '').trim() });
65
+ } catch {
66
+ return Object.freeze({ name: '', version: '' });
67
+ }
68
+ }
69
+
70
+ function normalizeIdentity(value = {}) {
71
+ return Object.freeze({
72
+ schema: String(value?.schema || 'tiinex.portable.tooling-runtime-identity.v1'),
73
+ packageName: String(value?.packageName || ''),
74
+ packageVersion: String(value?.packageVersion || ''),
75
+ representationSha256: String(value?.representationSha256 || ''),
76
+ sourceRepresentationSha256: String(value?.sourceRepresentationSha256 || ''),
77
+ runtimeFiles: Number(value?.runtimeFiles || 0),
78
+ runtimeBytes: Number(value?.runtimeBytes || 0),
79
+ entrypointSha256: String(value?.entrypointSha256 || ''),
80
+ enumerationPolicySha256: String(value?.enumerationPolicySha256 || '')
81
+ });
82
+ }
@@ -59,7 +59,7 @@ async function frontierFromLocalWorkspace(descriptor, options) {
59
59
  id: descriptor.label || `${options.side || 'input'}:${workspaceId}`,
60
60
  source: { kind: 'local-workspace', ref: root, snapshotContract: 'shared-handoff-manufacture-node-enumeration-v1' },
61
61
  workspaces: [workspaceFromEnumeration(workspaceId, enumeration, root)],
62
- boundary: 'Local source is enumerated by the same deterministic Node Workspace enumerator used by Handoff manufacture; excluded directories and symlink policy therefore remain shared.'
62
+ boundary: 'Local source is enumerated by the same deterministic Node Workspace enumerator used by Handoff manufacture; durable-source eligibility, generated-cache exclusions, configured path exclusions, and symlink policy therefore remain shared.'
63
63
  });
64
64
  } catch (error) {
65
65
  return createPortableSourceFrontier({ id: descriptor.label || options.side || '', state: 'qualification-error', source: { kind: 'local-workspace', ref: root }, workspaces: [{ workspaceId, state: 'unavailable', reason: 'local-workspace-read-failed', source: { kind: 'local-workspace', ref: root } }], findings: [portableFinding('error', 'portable.source-frontier.node.local-read-failed', 'Local Workspace source could not be enumerated.', { workspaceId, ref: root, detail: String(error?.message || error || '') })] });
@@ -180,6 +180,7 @@ function enumerationOptions(descriptor, options, workspaceId) {
180
180
  workspaceTitle: String(descriptor.workspaceTitle || descriptor.title || workspaceId),
181
181
  sourceMetadata: descriptor.sourceMetadata || {},
182
182
  excludeDirectories: descriptor.excludeDirectories || options.excludeDirectories,
183
+ excludeRelativePaths: descriptor.excludeRelativePaths || options.excludeRelativePaths,
183
184
  maxFiles: descriptor.maxFiles || options.maxFiles
184
185
  };
185
186
  }
@@ -0,0 +1,23 @@
1
+ import { prepareNodeSourceFrontier } from './sourceFrontierComparison.js';
2
+ import { provePortableSourceReconciliation } from '../../comparison/sourceFrontierReconciliationProof.js';
3
+
4
+ export async function prepareNodeSourceReconciliationProofInput(input = {}, options = {}) {
5
+ const [base, incoming, current, reconciled] = await Promise.all([
6
+ prepareNodeSourceFrontier(input.base, { ...options, side: 'base' }),
7
+ prepareNodeSourceFrontier(input.incoming, { ...options, side: 'incoming' }),
8
+ prepareNodeSourceFrontier(input.current, { ...options, side: 'current' }),
9
+ prepareNodeSourceFrontier(input.reconciled, { ...options, side: 'reconciled' })
10
+ ]);
11
+ return Object.freeze({
12
+ base,
13
+ incoming,
14
+ current,
15
+ reconciled,
16
+ dispositions: input.dispositions || []
17
+ });
18
+ }
19
+
20
+ export async function proveNodeSourceReconciliation(input = {}, options = {}) {
21
+ const normalized = await prepareNodeSourceReconciliationProofInput(input, options);
22
+ return provePortableSourceReconciliation(normalized);
23
+ }
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { enumerateNodeWorkspace } from './handoff.manufacture.enumeration.js';
3
3
  import { buildToolingBootstrapTransportFiles } from './handoff.manufacture.bootstrap.js';
4
+ import { qualifyToolingRuntimeSourceAlignment } from './handoff.manufacture.runtimeSource.js';
4
5
  import { inferWorkspaceTitle, normalizeAdditionalWorkspaceDescriptors, safeWorkspaceToken } from './handoff.manufacture.multiRoot.js';
5
6
  import { normalizeWorkspaceTargetBindings } from './handoff.manufacture.scope.js';
6
7
  import { normalizeHandoffCarrierLineage } from '../../handoff/carrierLineage.js';
@@ -28,6 +29,7 @@ export async function prepareNodeWorkspaceCarrierManufacturingInput(input = {},
28
29
  workspaceTitle: descriptor.title,
29
30
  sourceMetadata: descriptor.source || descriptor.sourceMetadata || {},
30
31
  excludeDirectories: descriptor.excludeDirectories || input.excludeDirectories || options.excludeDirectories,
32
+ excludeRelativePaths: descriptor.excludeRelativePaths || input.excludeRelativePaths || options.excludeRelativePaths,
31
33
  maxFiles: descriptor.maxFiles || input.maxFiles || options.maxFiles
32
34
  });
33
35
  if (enumeration.status !== 'qualified-complete') throw new Error(`portable.workspace-carrier.workspace-enumeration.${descriptor.id}.${enumeration.status}`);
@@ -47,6 +49,11 @@ export async function prepareNodeWorkspaceCarrierManufacturingInput(input = {},
47
49
  expected: input.expectedToolingBootstrap || null,
48
50
  maxFiles: input.bootstrapMaxFiles || options.bootstrapMaxFiles
49
51
  });
52
+ const runtimeSourceAlignment = await qualifyToolingRuntimeSourceAlignment({
53
+ runtimeIdentity: toolingBootstrap.runtimeIdentity,
54
+ localWorkspaces: enumerations.map((item) => Object.freeze({ id: item.materialization.id, root: item.root, materialization: item.materialization })),
55
+ maxFiles: input.bootstrapMaxFiles || options.bootstrapMaxFiles
56
+ });
50
57
  return Object.freeze({
51
58
  carrierMode: 'workspace',
52
59
  createdAt: String(input.createdAt || ''),
@@ -56,7 +63,7 @@ export async function prepareNodeWorkspaceCarrierManufacturingInput(input = {},
56
63
  carrierLineage: normalizeHandoffCarrierLineage(input.carrierLineage || null),
57
64
  carrierProfile: normalizeHandoffCarrierProfile(input.carrierProfile || null),
58
65
  toolingBootstrap: toolingBootstrap.summary,
59
- manufacturingEvidence: Object.freeze({ workspaceEnumerations: Object.freeze(enumerations.map((item) => Object.freeze({ id: item.materialization.id, root: item.root, evidence: item.evidence }))), toolingBootstrap: toolingBootstrap.summary }),
66
+ manufacturingEvidence: Object.freeze({ workspaceEnumerations: Object.freeze(enumerations.map((item) => Object.freeze({ id: item.materialization.id, root: item.root, evidence: item.evidence }))), toolingBootstrap: toolingBootstrap.summary, runtimeSourceAlignment }),
60
67
  verifyRoundtrip: input.verifyRoundtrip !== false
61
68
  });
62
69
  }
@@ -16,7 +16,8 @@ export function auditPortableRecord(record = {}, options = {}) {
16
16
  record,
17
17
  markdown: record.markdown,
18
18
  validationContractOverride: runtimeProjection?.state === 'qualified' ? runtimeProjection.compiledContract : null,
19
- schemaValidationAuthority: requireExactSchemaAuthority ? runtimeAuthority : null
19
+ schemaValidationAuthority: requireExactSchemaAuthority ? runtimeAuthority : null,
20
+ schemaReferenceContext: options.schemaReferenceContext || 'historical'
20
21
  });
21
22
  } catch (error) {
22
23
  const finding = portableFinding('error', 'portable.audit.exception', error?.message || 'Audit failed.', {
@@ -1,5 +1,6 @@
1
1
  import { sha256Hex, utf8Bytes } from '../../../export/package.bytes.js';
2
2
  import { portableFinding, summarizePortableFindings } from '../findings.js';
3
+ import { portableSourceEligibilityPolicy, qualifyPortableSourcePath } from '../source/sourceEligibility.js';
3
4
 
4
5
  export const PORTABLE_SOURCE_FRONTIER_SCHEMA_ID = 'tiinex.portable.source-frontier.v1';
5
6
  export const PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID = 'tiinex.portable.source-frontier-comparison.v1';
@@ -45,16 +46,34 @@ export function createPortableSourceFrontier(input = {}) {
45
46
  workspaces: Object.freeze(workspaces),
46
47
  findings: normalizedFindings,
47
48
  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
+ boundary: String(input.boundary || 'Eligible durable source-byte frontier only. Source-ineligible transport/runtime/cache bytes are outside frontier identity; Workspace/path equality does not establish semantic equivalence, authority, acceptance, deletion, or merge disposition.')
49
50
  });
50
51
  }
51
52
 
52
53
  export function createPortableWorkspaceSnapshot(entries = [], evidence = {}) {
53
54
  const findings = [];
54
- const normalized = normalizeEntries(entries, findings);
55
+ const selection = normalizeEntries(entries, findings);
55
56
  if (findings.length) return deepFreeze({ state: 'qualification-error', entries: Object.freeze([]), findings: Object.freeze(findings) });
57
+ const normalized = selection.entries;
56
58
  const totalBytes = normalized.reduce((sum, entry) => sum + entry.bytes, 0);
57
59
  const fingerprint = snapshotFingerprint(normalized);
60
+ const inheritedEvidence = serializableObject(evidence);
61
+ const priorEligibility = inheritedEvidence?.sourceEligibility && typeof inheritedEvidence.sourceEligibility === 'object'
62
+ ? inheritedEvidence.sourceEligibility
63
+ : null;
64
+ const upstreamSelection = priorEligibility?.upstreamSelection || (priorEligibility && Number(priorEligibility.excludedEntryCount || 0) > 0 ? portableSourceSelectionReceipt(priorEligibility) : null);
65
+ const sourceEligibility = Object.freeze({
66
+ schema: selection.policy.schema,
67
+ inputEntryCount: selection.inputEntryCount,
68
+ eligibleEntryCount: normalized.length,
69
+ excludedEntryCount: selection.excludedEntryCount,
70
+ excludedByReason: Object.freeze({ ...selection.excludedByReason }),
71
+ excludedDirectories: selection.policy.excludedDirectories,
72
+ excludedRelativePaths: selection.policy.excludedRelativePaths,
73
+ excludedFileSuffixes: selection.policy.excludedFileSuffixes,
74
+ ...(upstreamSelection ? { upstreamSelection: Object.freeze(upstreamSelection) } : {}),
75
+ boundary: selection.policy.boundary
76
+ });
58
77
  return deepFreeze({
59
78
  state: QUALIFIED,
60
79
  entryCount: normalized.length,
@@ -62,7 +81,7 @@ export function createPortableWorkspaceSnapshot(entries = [], evidence = {}) {
62
81
  fingerprint,
63
82
  fingerprintMethod: 'sha256-path-bytes-sha256-v1',
64
83
  entries: Object.freeze(normalized),
65
- evidence: Object.freeze(serializableObject(evidence)),
84
+ evidence: Object.freeze({ ...inheritedEvidence, sourceEligibility }),
66
85
  findings: Object.freeze([])
67
86
  });
68
87
  }
@@ -181,13 +200,28 @@ function normalizeWorkspace(raw, workspaceId, findings) {
181
200
  function normalizeEntries(entries, findings) {
182
201
  const out = [];
183
202
  const seen = new Set();
203
+ const policy = portableSourceEligibilityPolicy();
204
+ const excludedByReason = {};
205
+ let inputEntryCount = 0;
206
+ let excludedEntryCount = 0;
184
207
  for (const raw of entries || []) {
208
+ inputEntryCount += 1;
185
209
  const rawPath = String(raw?.path || raw?.innerPath || '').trim();
186
210
  const path = normalizePath(rawPath);
187
211
  if (unsafeRawPath(rawPath) || !path || unsafePath(path)) {
188
212
  findings.push(portableFinding('error', 'portable.source-frontier.entry.path-invalid', 'Comparable source entry has an unsafe or empty Workspace-relative path.', { path: rawPath }));
189
213
  continue;
190
214
  }
215
+ const eligibility = qualifyPortableSourcePath(path, {
216
+ excludeDirectories: policy.excludedDirectories,
217
+ excludeRelativePaths: policy.excludedRelativePaths,
218
+ entryKind: 'file'
219
+ });
220
+ if (!eligibility.eligible) {
221
+ excludedEntryCount += 1;
222
+ excludedByReason[eligibility.reason] = Number(excludedByReason[eligibility.reason] || 0) + 1;
223
+ continue;
224
+ }
191
225
  if (seen.has(path)) {
192
226
  findings.push(portableFinding('error', 'portable.source-frontier.entry.path-duplicate', 'Comparable source snapshot contains a duplicate normalized path.', { path }));
193
227
  continue;
@@ -202,7 +236,23 @@ function normalizeEntries(entries, findings) {
202
236
  out.push(Object.freeze({ path, bytes, sha256 }));
203
237
  }
204
238
  out.sort((a, b) => a.path.localeCompare(b.path));
205
- return out;
239
+ return Object.freeze({
240
+ entries: Object.freeze(out),
241
+ policy,
242
+ inputEntryCount,
243
+ excludedEntryCount,
244
+ excludedByReason: Object.freeze(Object.fromEntries(Object.entries(excludedByReason).sort(([a], [b]) => a.localeCompare(b))))
245
+ });
246
+ }
247
+
248
+ function portableSourceSelectionReceipt(value = {}) {
249
+ return {
250
+ schema: String(value.schema || ''),
251
+ inputEntryCount: Number(value.inputEntryCount || 0),
252
+ eligibleEntryCount: Number(value.eligibleEntryCount || 0),
253
+ excludedEntryCount: Number(value.excludedEntryCount || 0),
254
+ excludedByReason: Object.freeze({ ...serializableObject(value.excludedByReason || {}) })
255
+ };
206
256
  }
207
257
 
208
258
  function compareWorkspaceUnion(left, right) {