@tiinex/core 0.13.0 → 0.15.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 (29) hide show
  1. package/package.json +5 -5
  2. package/src/integrity/integrity.c14nV1.js +28 -0
  3. package/src/lineage/lineage.integrity.js +108 -22
  4. package/src/lineage/lineage.resolve.js +21 -14
  5. package/src/lineage/lineage.sourceScope.js +41 -12
  6. package/src/lineage/lineage.targetKeys.js +26 -7
  7. package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +47 -10
  8. package/src/tooling/portable/adapters/node/bootstrapCarrier.manufacture.js +18 -0
  9. package/src/tooling/portable/adapters/node/workspaceCarrier.manufacture.js +13 -3
  10. package/src/tooling/portable/bootstrap/tiinex.llm.bootstrap.md +2 -2
  11. package/src/tooling/portable/handoff/bootstrapCarrier.manufacture.js +56 -0
  12. package/src/tooling/portable/handoff/contracts/tiinex.handoff.package.v1.schema.md +340 -77
  13. package/src/tooling/portable/handoff/manufacture.js +3 -1
  14. package/src/tooling/portable/handoff/recipientV2.artifactFirst.build.js +73 -32
  15. package/src/tooling/portable/handoff/recipientV2.artifactFirst.closure.js +1 -1
  16. package/src/tooling/portable/handoff/recipientV2.artifactFirst.inspect.js +9 -1
  17. package/src/tooling/portable/handoff/recipientV2.artifactFirst.materials.js +28 -6
  18. package/src/tooling/portable/handoff/recipientV2.artifactFirst.shared.js +8 -0
  19. package/src/tooling/portable/handoff/recipientV2.coldProjection.js +11 -6
  20. package/src/tooling/portable/handoff/recipientV2.entryContract.js +12 -0
  21. package/src/tooling/portable/handoff/recipientV2.humanOutput.js +48 -6
  22. package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +55 -43
  23. package/src/tooling/portable/handoff/recipientV2.packageV1.contract.js +57 -31
  24. package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +63 -10
  25. package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +55 -16
  26. package/src/tooling/portable/handoff/recipientV2.packageV1.workspaceProjection.js +14 -1
  27. package/src/tooling/portable/handoff/recipientV2.topology.js +3 -4
  28. package/src/tooling/portable/handoff/recipientV2.topology.materials.js +14 -0
  29. package/src/tooling/portable/handoff/workspaceCarrier.manufacture.js +42 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiinex/core",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Shared host-neutral Tiinex implementation core for artifacts, schemas, validation, lineage, grounding, Handoffs, provenance and deterministic workflows.",
5
5
  "type": "module",
6
6
  "sideEffects": true,
@@ -168,12 +168,12 @@
168
168
  "type": "git",
169
169
  "url": "git+https://github.com/Tiinex/core.git"
170
170
  },
171
- "gitHead": "7e705cbd611919cc46659f9bcee0cf363a09c41c",
171
+ "gitHead": "d257bb3ae9e7c6361840d727ce5e359d1b6f6091",
172
172
  "tiinexRelease": {
173
173
  "policy": "tiinex.master-npm-release.v1",
174
- "sourceCommit": "7e705cbd611919cc46659f9bcee0cf363a09c41c",
175
- "sourceTree": "f6b7df8dfa2757bd05cd4d9f764cd0696cc23797",
174
+ "sourceCommit": "d257bb3ae9e7c6361840d727ce5e359d1b6f6091",
175
+ "sourceTree": "002c94ff43122ac8188de9514909dfdabd348803",
176
176
  "repository": "Tiinex/core",
177
- "previousVersion": "0.12.0"
177
+ "previousVersion": "0.14.0"
178
178
  }
179
179
  }
@@ -0,0 +1,28 @@
1
+ import { sha256Base64Url } from './integrity.c14nV2.js';
2
+
3
+ export const C14N_V1_METHOD_ID = 'sha256-base64url-c14n-v1';
4
+
5
+ export function canonicalC14nV1TargetState(markdown = '') {
6
+ const normalized = normalizeCanonicalSource(markdown);
7
+ const lines = normalized.split('\n');
8
+ const integrityHeading = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
9
+ const canonical = (integrityHeading < 0 ? lines : lines.slice(0, integrityHeading)).join('\n');
10
+ return Object.freeze({
11
+ state: 'computed',
12
+ method: C14N_V1_METHOD_ID,
13
+ canonical,
14
+ computedValue: sha256Base64Url(canonical),
15
+ integrityHeading
16
+ });
17
+ }
18
+
19
+ export function c14nV1TargetDigest(markdown = '') {
20
+ return canonicalC14nV1TargetState(markdown).computedValue;
21
+ }
22
+
23
+ function normalizeCanonicalSource(markdown = '') {
24
+ return String(markdown || '')
25
+ .replace(/\r\n?/g, '\n')
26
+ .replace(/[ \t]+$/gm, '')
27
+ .trimEnd();
28
+ }
@@ -1,3 +1,5 @@
1
+ import { C14N_V1_METHOD_ID, c14nV1TargetDigest } from '../integrity/integrity.c14nV1.js';
2
+ import { C14N_V2_METHOD_ID } from '../integrity/integrity.c14nV2.js';
1
3
  import { LineageResolutionStatus } from './lineage.model.js';
2
4
  import { canonicalPath, canonicalToken, provenanceTargetKeysForValue } from './lineage.targetKeys.js';
3
5
 
@@ -5,57 +7,138 @@ export function verifiedIntegrityMatch(match = null) {
5
7
  if (!match || match.ambiguous || match.selfReference || match.blocked) return match;
6
8
  return Object.assign({}, match, {
7
9
  status: LineageResolutionStatus.verified,
8
- diagnostics: [lineageDiagnostic('integrity.verified', 'Loaded parent self-integrity matches the child declaration.', { basis: 'checksum' })]
10
+ diagnostics: [lineageDiagnostic('integrity.verified', 'Loaded parent integrity matches the child declaration.', { basis: 'checksum' })]
9
11
  });
10
12
  }
11
13
 
12
- export function withParentIntegrityStatus(match = null, expectedIntegrityValues = []) {
13
- if (!match || !Array.isArray(expectedIntegrityValues) || !expectedIntegrityValues.length) return match;
14
+ export function withParentIntegrityStatus(match = null, expectedIntegrity = []) {
15
+ if (!match || !Array.isArray(expectedIntegrity) || !expectedIntegrity.length) return match;
14
16
  if (match.ambiguous || match.selfReference || match.blocked) return match;
15
17
  if (match.status === LineageResolutionStatus.verified || match.status === LineageResolutionStatus.mismatch || match.status === LineageResolutionStatus.probable) return match;
16
- const expected = expectedIntegrityValues.map(canonicalIntegrityValue).filter(Boolean);
17
- const actual = selfIntegrityValuesForNode(match);
18
- if (!actual.length) {
18
+ const expectations = normalizeIntegrityExpectations(expectedIntegrity);
19
+ if (!expectations.length) return match;
20
+ const checks = expectations.map((expectation) => verifyParentIntegrityExpectation(match, expectation));
21
+ const verified = checks.find((check) => check.state === 'verified');
22
+ if (verified) {
19
23
  return Object.assign({}, match, {
20
- status: LineageResolutionStatus.probable,
21
- diagnostics: [lineageDiagnostic('integrity.unavailable', 'Declared parent found, but loaded parent has no self-integrity value to verify against.', { basis: match.method || '' })]
24
+ status: LineageResolutionStatus.verified,
25
+ diagnostics: [lineageDiagnostic('integrity.verified', verified.message, { basis: verified.basis, method: verified.method, expected: verified.expected, actual: verified.actual })]
22
26
  });
23
27
  }
24
- const matched = expected.some((value) => actual.includes(value));
25
- if (matched) {
28
+ const comparable = checks.filter((check) => check.state === 'mismatch');
29
+ if (!comparable.length) {
26
30
  return Object.assign({}, match, {
27
- status: LineageResolutionStatus.verified,
28
- diagnostics: [lineageDiagnostic('integrity.verified', 'Declared parent found and self-integrity matches the child declaration.', { basis: match.method || '' })]
31
+ status: LineageResolutionStatus.probable,
32
+ diagnostics: [lineageDiagnostic('integrity.unavailable', 'Declared parent found, but the loaded parent does not expose the method-specific material required to verify the child declaration.', { basis: match.method || '', methods: expectations.map((item) => item.method).filter(Boolean).join(', ') })]
29
33
  });
30
34
  }
31
35
  return Object.assign({}, match, {
32
36
  status: LineageResolutionStatus.mismatch,
33
- diagnostics: [lineageDiagnostic('integrity.mismatch', 'Declared parent found, but loaded parent self-integrity does not match the child declaration.', { basis: match.method || '', expected: expected.join(', '), actual: actual.join(', ') })]
37
+ diagnostics: comparable.map((check) => lineageDiagnostic('integrity.mismatch', check.message, { basis: check.basis, method: check.method, expected: check.expected, actual: check.actual }))
34
38
  });
35
39
  }
36
40
 
37
- export function parentIntegrityValuesForTarget(node = {}, target = '') {
41
+ export function parentIntegrityExpectationsForTarget(node = {}, target = '') {
38
42
  const targetKeys = lineageTargetComparisonKeys(target);
39
43
  if (!targetKeys.length) return [];
40
44
  const entries = integrityEntriesForNode(node);
41
- const values = [];
45
+ const expectations = [];
42
46
  for (const entry of entries) {
43
47
  const towards = String(entry.towards || '').trim();
44
48
  if (!towards || /^self$/i.test(towards)) continue;
45
49
  const entryKeys = lineageTargetComparisonKeys(towards);
46
50
  if (!entryKeys.some((key) => targetKeys.includes(key))) continue;
47
51
  const value = canonicalIntegrityValue(entry.value);
48
- if (value) values.push(value);
52
+ if (!value) continue;
53
+ expectations.push(Object.freeze({ method: canonicalIntegrityMethod(entry.method || entry.declaredMethod || ''), value, towards }));
49
54
  }
50
- return Array.from(new Set(values));
55
+ return dedupeExpectations(expectations);
56
+ }
57
+
58
+ export function parentIntegrityValuesForTarget(node = {}, target = '') {
59
+ return parentIntegrityExpectationsForTarget(node, target).map((entry) => entry.value);
51
60
  }
52
61
 
53
62
  export function selfIntegrityValuesForNode(node = {}) {
54
- const entries = integrityEntriesForNode(node);
55
- return Array.from(new Set(entries
63
+ return Array.from(new Set(selfIntegrityEntriesForNode(node).map((entry) => canonicalIntegrityValue(entry.value)).filter(Boolean)));
64
+ }
65
+
66
+ export function selfIntegrityEntriesForNode(node = {}) {
67
+ return integrityEntriesForNode(node)
56
68
  .filter((entry) => /^self$/i.test(String(entry.towards || '').trim()))
57
- .map((entry) => canonicalIntegrityValue(entry.value))
58
- .filter(Boolean)));
69
+ .map((entry) => Object.freeze({
70
+ method: canonicalIntegrityMethod(entry.method || entry.declaredMethod || ''),
71
+ value: canonicalIntegrityValue(entry.value),
72
+ towards: 'self'
73
+ }))
74
+ .filter((entry) => entry.value);
75
+ }
76
+
77
+ function verifyParentIntegrityExpectation(match = {}, expectation = {}) {
78
+ const method = canonicalIntegrityMethod(expectation.method || '');
79
+ const expected = canonicalIntegrityValue(expectation.value);
80
+ if (!expected) return unavailableCheck(method, expected, 'integrity-value-missing');
81
+ const markdown = String(match?.record?.markdown || match?.markdown || '');
82
+ if (method === C14N_V1_METHOD_ID) {
83
+ if (!markdown) return unavailableCheck(method, expected, 'target-markdown-unavailable');
84
+ const actual = c14nV1TargetDigest(markdown);
85
+ return Object.freeze({
86
+ state: actual === expected ? 'verified' : 'mismatch',
87
+ method,
88
+ expected,
89
+ actual,
90
+ basis: C14N_V1_METHOD_ID,
91
+ message: actual === expected
92
+ ? 'Declared parent found and direct c14n-v1 canonicalization of the resolved target matches the child declaration.'
93
+ : 'Declared parent found, but direct c14n-v1 canonicalization of the resolved target does not match the child declaration.'
94
+ });
95
+ }
96
+
97
+ const selfEntries = selfIntegrityEntriesForNode(match);
98
+ const comparableEntries = method ? selfEntries.filter((entry) => entry.method === method) : selfEntries;
99
+ const actualValues = Array.from(new Set(comparableEntries.map((entry) => entry.value).filter(Boolean)));
100
+ if (!actualValues.length) return unavailableCheck(method, expected, 'target-self-integrity-unavailable');
101
+ const matched = actualValues.includes(expected);
102
+ return Object.freeze({
103
+ state: matched ? 'verified' : 'mismatch',
104
+ method,
105
+ expected,
106
+ actual: actualValues.join(', '),
107
+ basis: method || match.method || 'target-self-integrity',
108
+ message: matched
109
+ ? 'Declared parent found and method-compatible target self-integrity matches the child declaration.'
110
+ : 'Declared parent found, but method-compatible target self-integrity does not match the child declaration.'
111
+ });
112
+ }
113
+
114
+ function unavailableCheck(method = '', expected = '', reason = '') {
115
+ return Object.freeze({ state: 'unavailable', method, expected, actual: '', basis: method || reason, reason, message: 'Method-specific target integrity material is unavailable.' });
116
+ }
117
+
118
+ function normalizeIntegrityExpectations(values = []) {
119
+ const out = [];
120
+ for (const item of values) {
121
+ if (item && typeof item === 'object') {
122
+ const value = canonicalIntegrityValue(item.value);
123
+ if (value) out.push(Object.freeze({ method: canonicalIntegrityMethod(item.method || item.declaredMethod || ''), value, towards: String(item.towards || '') }));
124
+ continue;
125
+ }
126
+ const value = canonicalIntegrityValue(item);
127
+ if (value) out.push(Object.freeze({ method: '', value, towards: '' }));
128
+ }
129
+ return dedupeExpectations(out);
130
+ }
131
+
132
+ function dedupeExpectations(entries = []) {
133
+ const seen = new Set();
134
+ const out = [];
135
+ for (const entry of entries) {
136
+ const key = `${entry.method}\u0000${entry.value}\u0000${entry.towards}`;
137
+ if (seen.has(key)) continue;
138
+ seen.add(key);
139
+ out.push(entry);
140
+ }
141
+ return out;
59
142
  }
60
143
 
61
144
  function integrityEntriesForNode(node = {}) {
@@ -116,6 +199,10 @@ export function canonicalIntegrityValue(value = '') {
116
199
  return String(value || '').trim();
117
200
  }
118
201
 
202
+ export function canonicalIntegrityMethod(value = '') {
203
+ return stripMarkdown(String(value || '').trim()).toLowerCase();
204
+ }
205
+
119
206
  function normalizeIntegrityField(value = '') {
120
207
  const raw = String(value || '').trim();
121
208
  const link = raw.match(/^\[([^\]]*)\]\(([^)]+)\)$/);
@@ -130,4 +217,3 @@ function stripMarkdown(value = '') {
130
217
  function lineageDiagnostic(code, message, extra = {}) {
131
218
  return Object.freeze(Object.assign({ code, message }, extra || {}));
132
219
  }
133
-
@@ -3,7 +3,7 @@ import { issueLocalPathKeysForNode, issueLocalPathMatches } from './lineage.gith
3
3
  import { filterGitHubIssueCommentCandidatesForTarget, githubIssueCommentIdFromValue, githubIssueCommentIdsForNode } from './lineage.githubIssueComment.js';
4
4
  import { filterExactGitHubIssueCandidatesForTarget } from './lineage.githubIssueTarget.js';
5
5
  import { isDotRelativeReference, isSimpleRelativeReference, isUrlLike, relativeCandidatePath, resolveCandidateNodes } from './lineage.candidateResolution.js';
6
- import { canonicalIntegrityValue, parentIntegrityValuesForTarget, selfIntegrityValuesForNode, verifiedIntegrityMatch, withParentIntegrityStatus } from './lineage.integrity.js';
6
+ import { canonicalIntegrityValue, parentIntegrityExpectationsForTarget, parentIntegrityValuesForTarget, selfIntegrityValuesForNode, withParentIntegrityStatus } from './lineage.integrity.js';
7
7
  import { canonicalPath, canonicalToken, githubRepoRelativePathFromUrl, provenanceTargetKeysForValue, sourceKeyFromTarget } from './lineage.targetKeys.js';
8
8
  import { declaredParentBindingTargetValuesForNode, isSyntheticPublicationLineageNode } from './lineage.parentBinding.js';
9
9
  import { exactUnloadedParent } from './lineage.parentAuthority.js';
@@ -22,7 +22,7 @@ export function resolveLineage(artifacts = [], options = {}) {
22
22
  }
23
23
  const traceTarget = targets.find((target) => target.kind === LineageEdgeKind.parent);
24
24
  const originTarget = targets.find((target) => target.kind === LineageEdgeKind.origin);
25
- const parentMatch = traceTarget ? resolveTarget(traceTarget.value, index, node, { expectedIntegrityValues: traceTarget.integrityValues, targetKind: LineageEdgeKind.parent }) : null;
25
+ const parentMatch = traceTarget ? resolveTarget(traceTarget.value, index, node, { expectedIntegrityValues: traceTarget.integrityValues, expectedIntegrity: traceTarget.integrityExpectations, targetKind: LineageEdgeKind.parent }) : null;
26
26
  const originMatch = originTarget ? resolveTarget(originTarget.value, index, node, { targetKind: LineageEdgeKind.origin }) : null;
27
27
  if (parentMatch?.selfReference) {
28
28
  findings.push(createLineageFinding('lineage.parent.selfReference', 'Declared Parent Trace resolves to the declaring artifact itself; no parent edge was created.', 'warning', { nodeId: node.id, target: traceTarget.value }));
@@ -170,7 +170,10 @@ export function resolveLineage(artifacts = [], options = {}) {
170
170
  }
171
171
  function declaredTargetsFor(node = {}) {
172
172
  const targets = [];
173
- if (node.trace) targets.push({ kind: LineageEdgeKind.parent, value: node.trace, integrityValues: parentIntegrityValuesForTarget(node, node.trace) });
173
+ if (node.trace) {
174
+ const integrityExpectations = parentIntegrityExpectationsForTarget(node, node.trace);
175
+ targets.push({ kind: LineageEdgeKind.parent, value: node.trace, integrityExpectations, integrityValues: integrityExpectations.length ? integrityExpectations.map((entry) => entry.value) : parentIntegrityValuesForTarget(node, node.trace) });
176
+ }
174
177
  if (node.origin) targets.push({ kind: LineageEdgeKind.origin, value: node.origin });
175
178
  return targets;
176
179
  }
@@ -228,23 +231,27 @@ function resolveTarget(target, index, declaringNode = null, options = {}) {
228
231
  const raw = String(target || '').trim();
229
232
  if (!raw) return null;
230
233
 
231
- const expectedIntegrityValues = Array.isArray(options.expectedIntegrityValues) ? options.expectedIntegrityValues.map(canonicalIntegrityValue).filter(Boolean) : [];
232
- if (expectedIntegrityValues.length) {
233
- const integrityMatch = resolveIntegrityTarget(expectedIntegrityValues, index, declaringNode);
234
- if (integrityMatch) return integrityMatch;
235
- }
236
-
234
+ const expectedIntegrity = Array.isArray(options.expectedIntegrity) && options.expectedIntegrity.length
235
+ ? options.expectedIntegrity
236
+ : Array.isArray(options.expectedIntegrityValues) ? options.expectedIntegrityValues : [];
237
+ const expectedIntegrityValues = expectedIntegrity.map((entry) => canonicalIntegrityValue(entry && typeof entry === 'object' ? entry.value : entry)).filter(Boolean);
237
238
  const token = canonicalToken(raw);
238
239
  const path = canonicalPath(raw);
239
240
  const urlFilePath = githubRepoRelativePathFromUrl(raw);
240
241
  const urlSourceKey = sourceKeyFromTarget(raw);
242
+ const targetSourceConstraint = urlSourceKey ? sourceConstraintFromTarget(raw) : null;
243
+ if (expectedIntegrityValues.length && !targetSourceConstraint?.ref) {
244
+ const integrityMatch = resolveIntegrityTarget(expectedIntegrityValues, index, declaringNode);
245
+ if (integrityMatch) return withParentIntegrityStatus(integrityMatch, expectedIntegrity);
246
+ }
247
+
241
248
  const declaringConstraint = sourceConstraintFromNode(declaringNode);
242
249
  const relative = relativeCandidatePath(raw, declaringNode);
243
250
  const simpleRelative = isSimpleRelativeReference(raw);
244
251
  const dotRelative = isDotRelativeReference(raw);
245
252
  const urlLike = isUrlLike(raw);
246
253
  const recordToken = /^record:/i.test(raw);
247
- const finalize = (match) => withParentIntegrityStatus(match, expectedIntegrityValues);
254
+ const finalize = (match) => withParentIntegrityStatus(match, expectedIntegrity);
248
255
 
249
256
  const resolveDirectToken = () => {
250
257
  const directTokenCandidates = [
@@ -297,7 +304,7 @@ function resolveTarget(target, index, declaringNode = null, options = {}) {
297
304
  continue;
298
305
  }
299
306
  const sourceKey = sourceKeyFromTarget(binding.raw);
300
- const constraint = sourceKey ? sourceConstraintFromTarget(sourceKey) : declaringConstraint;
307
+ const constraint = sourceKey ? sourceConstraintFromTarget(binding.raw) : declaringConstraint;
301
308
  const exact = exactPathMatches(binding.filePath, index, constraint, Boolean(constraint.hasConstraint));
302
309
  const resolvedExact = resolveCandidateNodes(exact, 'declared-parent-path', declaringNode);
303
310
  if (resolvedExact) return finalize(resolvedExact);
@@ -357,12 +364,12 @@ function resolveTarget(target, index, declaringNode = null, options = {}) {
357
364
  const direct = resolveDirectToken();
358
365
  if (direct) return finalize(direct);
359
366
 
360
- const pathConstraint = urlSourceKey ? sourceConstraintFromTarget(urlSourceKey) : declaringConstraint;
367
+ const pathConstraint = targetSourceConstraint || declaringConstraint;
361
368
  const exact = exactPathMatches(path, index, pathConstraint, Boolean(pathConstraint.hasConstraint));
362
369
  const resolvedExact = resolveCandidateNodes(exact, 'path', declaringNode);
363
370
  if (resolvedExact) return finalize(resolvedExact);
364
371
 
365
- const suffixConstraint = urlSourceKey ? sourceConstraintFromTarget(urlSourceKey) : declaringConstraint;
372
+ const suffixConstraint = targetSourceConstraint || declaringConstraint;
366
373
  const suffixCandidates = [
367
374
  ['path-suffix', findPathSuffixMatches(path, index.byPath, suffixConstraint, Boolean(urlSourceKey))],
368
375
  ['source-path-suffix', findPathSuffixMatches(path, index.bySourcePath, suffixConstraint, Boolean(urlSourceKey))]
@@ -384,5 +391,5 @@ function resolveIntegrityTarget(expectedIntegrityValues = [], index = {}, declar
384
391
  }
385
392
  const resolved = resolveCandidateNodes(nodes, 'integrity-self-hash', declaringNode);
386
393
  if (!resolved || resolved.ambiguous || resolved.selfReference || resolved.blocked) return resolved;
387
- return verifiedIntegrityMatch(resolved);
394
+ return resolved;
388
395
  }
@@ -1,4 +1,4 @@
1
- import { canonicalPath, normalizeRef, normalizeRepoKey } from './lineage.targetKeys.js';
1
+ import { canonicalPath, githubFileIdentityFromUrl, normalizeRef, normalizeRepoKey } from './lineage.targetKeys.js';
2
2
 
3
3
  export function exactPathMatches(path, index, constraint = {}, strictSource = false) {
4
4
  const source = [
@@ -27,18 +27,47 @@ export function findPathSuffixMatches(targetPath, pathIndex = new Map(), constra
27
27
  }
28
28
 
29
29
  export function sourceConstraintFromNode(node = {}) {
30
- const source = node?.record?.source || {};
31
- const adapterId = String(source.adapterId || '').trim().toLowerCase();
30
+ const record = node?.record || node || {};
31
+ const source = record.source || {};
32
+ const provenance = sourceIdentityFromRecord(record);
32
33
  const sourceId = String(source.id || '').trim();
33
- if (adapterId === 'local' || sourceId === 'local' || source.kind === 'local-session') return { hasConstraint: false, sourceId: '', repo: '', ref: '', adapterId: '' };
34
- const repo = normalizeRepoKey(source.repo || source.repository || source.config?.repo || '');
35
- const ref = normalizeRef(source.ref || source.config?.ref || '');
36
- return { hasConstraint: Boolean(sourceId || repo || adapterId), sourceId, repo, ref, adapterId };
34
+ const adapterId = String(source.adapterId || source.adapter || provenance.adapterId || '').trim().toLowerCase();
35
+ const repo = normalizeRepoKey(source.repo || source.repository || source.config?.repo || provenance.repo || '');
36
+ const ref = normalizeRef(source.commit || source.ref || source.config?.commit || source.config?.ref || provenance.ref || '');
37
+ const isLocal = adapterId === 'local' || sourceId === 'local' || source.kind === 'local-session';
38
+ if (isLocal && !repo && !ref) return { hasConstraint: false, sourceId: '', repo: '', ref: '', adapterId: '', exactRef: false };
39
+ return { hasConstraint: Boolean(sourceId || repo || adapterId || ref), sourceId, repo, ref, adapterId, exactRef: Boolean(ref) };
37
40
  }
38
41
 
39
- export function sourceConstraintFromTarget(repo = '') {
40
- const key = normalizeRepoKey(repo);
41
- return { hasConstraint: Boolean(key), repo: key, sourceId: '', ref: '', adapterId: 'github' };
42
+ export function sourceConstraintFromTarget(value = '') {
43
+ const identity = githubFileIdentityFromUrl(value);
44
+ if (identity.repo) return { hasConstraint: true, repo: identity.repo, ref: identity.ref, sourceId: '', adapterId: 'github', exactRef: Boolean(identity.ref) };
45
+ const key = normalizeRepoKey(value);
46
+ return { hasConstraint: Boolean(key), repo: key, ref: '', sourceId: '', adapterId: key ? 'github' : '', exactRef: false };
47
+ }
48
+
49
+ function sourceIdentityFromRecord(record = {}) {
50
+ const sourceTarget = record.sourceTarget || {};
51
+ const snapshot = record.snapshot || {};
52
+ const target = snapshot.target || {};
53
+ const values = [
54
+ record.recoveredFromUrl,
55
+ record.sourceOrigin,
56
+ record.rawUrl,
57
+ record.browseUrl,
58
+ sourceTarget.inputTarget,
59
+ sourceTarget.rawUrl,
60
+ sourceTarget.browseUrl,
61
+ snapshot.sourceUrl,
62
+ target.canonicalUrl,
63
+ target.html_url,
64
+ target.url
65
+ ];
66
+ for (const value of values) {
67
+ const identity = githubFileIdentityFromUrl(value);
68
+ if (identity.repo) return { ...identity, adapterId: 'github' };
69
+ }
70
+ return { repo: '', ref: '', path: '', adapterId: '' };
42
71
  }
43
72
 
44
73
  function filterBySource(nodes = [], constraint = {}, strictSource = false) {
@@ -46,7 +75,7 @@ function filterBySource(nodes = [], constraint = {}, strictSource = false) {
46
75
  if (!constraint?.hasConstraint) return items;
47
76
  const filtered = items.filter((node) => nodeMatchesSourceConstraint(node, constraint));
48
77
  if (filtered.length) return filtered;
49
- if (strictSource && items.length && items.every((node) => !sourceConstraintFromNode(node).hasConstraint)) return items;
78
+ if (strictSource && !constraint.ref && items.length && items.every((node) => !sourceConstraintFromNode(node).hasConstraint)) return items;
50
79
  return [];
51
80
  }
52
81
 
@@ -55,7 +84,7 @@ function nodeMatchesSourceConstraint(node = {}, constraint = {}) {
55
84
  if (constraint.sourceId && candidate.sourceId && constraint.sourceId !== candidate.sourceId) return false;
56
85
  if (constraint.adapterId && candidate.adapterId && constraint.adapterId !== candidate.adapterId) return false;
57
86
  if (constraint.repo && candidate.repo !== constraint.repo) return false;
58
- if (constraint.ref && candidate.ref && constraint.ref !== candidate.ref) return false;
87
+ if (constraint.ref && candidate.ref !== constraint.ref) return false;
59
88
  if (constraint.repo && !candidate.repo) return false;
60
89
  if (constraint.adapterId && !candidate.adapterId) return false;
61
90
  return true;
@@ -31,14 +31,29 @@ export function provenanceTargetKeysForValue(value = '') {
31
31
  }
32
32
  return keys;
33
33
  }
34
- export function githubRepoRelativePathFromUrl(value = '') {
35
- try { const u = new URL(String(value || '').trim()), p = u.pathname.split('/').filter(Boolean), h = u.hostname.toLowerCase();
36
- if (h === 'raw.githubusercontent.com' && p.length >= 4) return p.slice(3).join('/');
37
- if ((h === 'github.com' || h.endsWith('.github.com')) && p.length >= 5 && p[2] === 'blob') return p.slice(4).join('/');
34
+
35
+ export function githubFileIdentityFromUrl(value = '') {
36
+ try {
37
+ const url = new URL(String(value || '').trim());
38
+ const host = url.hostname.toLowerCase();
39
+ const parts = url.pathname.split('/').filter(Boolean);
40
+ if (host === 'raw.githubusercontent.com' && parts.length >= 4) {
41
+ return Object.freeze({ repo: normalizeRepoKey(`${parts[0]}/${parts[1]}`), ref: normalizeRef(parts[2]), path: normalizePathParts(parts.slice(3)) });
42
+ }
43
+ if ((host === 'github.com' || host.endsWith('.github.com')) && parts.length >= 5 && parts[2] === 'blob') {
44
+ return Object.freeze({ repo: normalizeRepoKey(`${parts[0]}/${parts[1]}`), ref: normalizeRef(parts[3]), path: normalizePathParts(parts.slice(4)) });
45
+ }
38
46
  } catch (_) {}
39
- return '';
47
+ return Object.freeze({ repo: '', ref: '', path: '' });
40
48
  }
49
+
50
+ export function githubRepoRelativePathFromUrl(value = '') {
51
+ return githubFileIdentityFromUrl(value).path;
52
+ }
53
+
41
54
  export function sourceKeyFromTarget(value = '') {
55
+ const identity = githubFileIdentityFromUrl(value);
56
+ if (identity.repo) return identity.repo;
42
57
  try {
43
58
  const url = new URL(String(value || ''));
44
59
  const parts = url.pathname.split('/').filter(Boolean);
@@ -53,7 +68,7 @@ export function normalizeRepoKey(value = '') {
53
68
  return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : '';
54
69
  }
55
70
  export function normalizeRef(value = '') {
56
- return String(value || '').trim().toLowerCase();
71
+ return String(value || '').trim();
57
72
  }
58
73
  export function canonicalToken(value = '') {
59
74
  return String(value || '').trim().replace(/^record:/i, 'record:').replace(/\s+/g, '');
@@ -70,8 +85,12 @@ export function canonicalPath(value = '') {
70
85
  raw = url.pathname.replace(/^\/+/, '');
71
86
  } catch (e) {}
72
87
  }
88
+ return normalizePathParts(raw.replace(/\\/g, '/').split('/'));
89
+ }
90
+
91
+ function normalizePathParts(parts = []) {
73
92
  const out = [];
74
- for (const part of raw.replace(/\\/g, '/').split('/')) {
93
+ for (const part of parts) {
75
94
  if (!part || part === '.') continue;
76
95
  if (part === '..') out.pop();
77
96
  else out.push(part);
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { prepareNodeHandoffManufacturingInput } from '../node/handoff.manufacture.js';
4
4
  import { prepareNodeWorkspaceCarrierManufacturingInput } from '../node/workspaceCarrier.manufacture.js';
5
+ import { prepareNodeBootstrapCarrierManufacturingInput } from '../node/bootstrapCarrier.manufacture.js';
5
6
  import { projectHandoffHumanOutput } from '../../handoff/carrierProjection.js';
6
7
  import { writePortableRuntimePackageZip } from '../../output/node.zip.js';
7
8
  import { writeRecipientFacingV2PackageZip } from '../../output/recipientV2.zip.js';
@@ -16,8 +17,9 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
16
17
  const flags = parsed.flags || {};
17
18
  const workspaceRoot = flags.workspace || parsed.positionals?.[0] || '.';
18
19
  const carrierMode = String(flags['carrier-mode'] || 'handoff').trim().toLowerCase();
19
- if (!['handoff', 'workspace'].includes(carrierMode)) throw new Error(`portable.cli.handoff-carrier.carrier-mode.invalid:${carrierMode}`);
20
+ if (!['handoff', 'workspace', 'bootstrap'].includes(carrierMode)) throw new Error(`portable.cli.handoff-carrier.carrier-mode.invalid:${carrierMode}`);
20
21
  if (carrierMode === 'workspace') return prepareWorkspaceCarrierCliCommand(flags, workspaceRoot, runtime);
22
+ if (carrierMode === 'bootstrap') return prepareBootstrapCarrierCliCommand(flags, runtime);
21
23
  const continuationState = parsed.surfaceCommand === 'handoff'
22
24
  ? await readGroundContinuationState(workspaceRoot)
23
25
  : {};
@@ -146,13 +148,15 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
146
148
  }
147
149
 
148
150
  export async function materializeHandoffManufactureCliOutput(result = {}, flags = {}) {
149
- const workspaceMode = String(result.carrierProjection?.mode || '') === 'workspace';
150
- let humanOutput = workspaceMode ? projectWorkspaceCarrierHumanOutput(result, flags) : projectHandoffHumanOutput({
151
+ const carrierMode = String(result.carrierProjection?.mode || '');
152
+ const workspaceMode = carrierMode === 'workspace';
153
+ const bootstrapMode = carrierMode === 'bootstrap';
154
+ let humanOutput = workspaceMode ? projectWorkspaceCarrierHumanOutput(result, flags) : bootstrapMode ? projectBootstrapCarrierHumanOutput(result, flags) : projectHandoffHumanOutput({
151
155
  projection: result.carrierProjection || {},
152
156
  route: flags.route || '',
153
157
  collisionInstance: flags['collision-instance'] || 1
154
158
  });
155
- if (!workspaceMode && result.bundle?.transportFormat) humanOutput = projectRecipientV2HumanOutput(humanOutput, result.inspection || {});
159
+ if (result.bundle?.transportFormat) humanOutput = projectRecipientV2HumanOutput(humanOutput, result.inspection || {});
156
160
  const wantsWrite = Boolean(flags.output || flags['output-dir']);
157
161
  const blocked = result.status === 'blocked' || result.transportExecutable === false || Number(result.findingSummary?.counts?.error || 0) > 0;
158
162
  if (!wantsWrite || blocked) return summarizeHandoffManufactureCliOutput(result, {}, humanOutput, null);
@@ -163,7 +167,6 @@ export async function materializeHandoffManufactureCliOutput(result = {}, flags
163
167
  const writeReceipt = writeBundle?.transportFormat
164
168
  ? await writeRecipientFacingV2PackageZip(writeBundle, target, writeBundle === result.bundle ? { inspection: result.inspection } : {})
165
169
  : await writePortableRuntimePackageZip(writeBundle, target);
166
- if (workspaceMode && flags['transport-text']) throw new Error('portable.cli.workspace-carrier.transport-text.unavailable');
167
170
  const transportTextReceipt = flags['transport-text'] ? await writeTransportTextSidecar(humanOutput, target, flags['transport-text']) : null;
168
171
  return summarizeHandoffManufactureCliOutput(result, writeReceipt, humanOutput, transportTextReceipt);
169
172
  }
@@ -174,6 +177,7 @@ async function prepareWorkspaceCarrierCliCommand(flags = {}, workspaceRoot = '.'
174
177
  const expectedToolingBootstrap = await readOptionalJson(flags['tooling-bootstrap-manifest']);
175
178
  const workspaceDescriptorValue = await readOptionalJson(flags['workspace-roots'] || flags['workspace-descriptors']);
176
179
  const workspaceTargetValue = await readOptionalJson(flags['workspace-targets']);
180
+ const workspaceScopeValue = await readOptionalJson(flags['workspace-scopes']);
177
181
  const additionalWorkspaces = [...splitFlag(flags['additional-workspaces']), ...descriptorArray(workspaceDescriptorValue, 'workspaces')];
178
182
  const verifyRoundtrip = !flags['no-roundtrip'];
179
183
  const carrierProfile = selectCarrierProfile({ operator: operatorCarrierProfile, runtime: runtime.defaultCarrierProfile || null });
@@ -185,6 +189,8 @@ async function prepareWorkspaceCarrierCliCommand(flags = {}, workspaceRoot = '.'
185
189
  workspaceTitle: flags['workspace-title'] || flags.title || '',
186
190
  workspaceTargetPath: flags['workspace-target'] || flags['workspace-artifact'] || '',
187
191
  workspaceTargets: workspaceTargetValue,
192
+ workspaceScopes: descriptorArray(workspaceScopeValue, 'scopes').length ? descriptorArray(workspaceScopeValue, 'scopes') : workspaceScopeValue,
193
+ materialRepresentationWorkspaceIds: splitFlag(flags['material-representation-workspaces'] || flags['generic-material-workspaces']),
188
194
  toolingBootstrap: flags['tooling-bootstrap'] || 'embedded',
189
195
  expectedToolingBootstrap,
190
196
  maxFiles: flags['max-files'],
@@ -198,6 +204,37 @@ async function prepareWorkspaceCarrierCliCommand(flags = {}, workspaceRoot = '.'
198
204
  return { input, options: { verifyRoundtrip, packageInput: { builtAt: flags['built-at'] || undefined } } };
199
205
  }
200
206
 
207
+ async function prepareBootstrapCarrierCliCommand(flags = {}, runtime = {}) {
208
+ if (flags.handoff || flags.route || flags.routes || flags['handoff-routes'] || flags['workspace-routes'] || flags.workspace || flags['workspace-target'] || flags['workspace-targets'] || flags['workspace-roots'] || flags['workspace-descriptors']) throw new Error('portable.cli.bootstrap-carrier.source-material-or-route.forbidden');
209
+ const operatorCarrierProfile = await readOptionalJson(flags['carrier-profile']);
210
+ const expectedToolingBootstrap = await readOptionalJson(flags['tooling-bootstrap-manifest']);
211
+ const verifyRoundtrip = !flags['no-roundtrip'];
212
+ const carrierProfile = selectCarrierProfile({ operator: operatorCarrierProfile, runtime: runtime.defaultCarrierProfile || null });
213
+ const input = await prepareNodeBootstrapCarrierManufacturingInput({
214
+ toolingBootstrap: flags['tooling-bootstrap'] || 'embedded', expectedToolingBootstrap, bootstrapMaxFiles: flags['bootstrap-max-files'], verifyRoundtrip, createdAt: flags['built-at'] || undefined,
215
+ carrierLineage: Object.freeze({ ...initialHandoffCarrierLineage(), checkpointKind: 'progression', majorReason: '' }), carrierProfile
216
+ }, runtime);
217
+ return { input, options: { verifyRoundtrip, packageInput: { builtAt: flags['built-at'] || undefined } } };
218
+ }
219
+
220
+ function projectBootstrapCarrierHumanOutput(result = {}, flags = {}) {
221
+ const projection = result.carrierProjection || {};
222
+ const ready = result.status === 'ready' && projection.status === 'ready' && projection.mode === 'bootstrap' && (projection.routes || []).length === 0 && (projection.workspaces || []).length === 0;
223
+ const dimension = String(projection.lineage?.dimension || '001');
224
+ const projectedFilename = String(flags['projected-filename'] || flags.projectedFilename || '').trim();
225
+ const filename = projectedFilename || `tiinex-${dimension}.handoff-package.zip`;
226
+ if (filename !== filename.trim() || !filename.endsWith('.handoff-package.zip') || /[<>:"/\\|?*\x00-\x1f\x7f]/.test(filename) || /[. ]$/.test(filename) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(filename) || new TextEncoder().encode(filename).byteLength > 255) throw new Error('portable.cli.bootstrap-carrier.filename.invalid');
227
+ return Object.freeze({
228
+ schema: 'tiinex.portable.handoff-human-output.v1', status: ready ? 'ready' : 'blocked',
229
+ primary: ready ? Object.freeze({ kind: 'bootstrap-package', filename, dimension, parentDimension: String(projection.lineage?.parentDimension || ''), checkpointKind: String(projection.lineage?.checkpointKind || ''), routeId: '', workspaceId: '', workspaceRelativeHandoffPath: '', collisionInstance: 1, singleHumanTransportChoice: true }) : null,
230
+ normalInlineRouting: ready ? Object.freeze({ kind: 'transport-text', content: '', normalEmission: true, requiredForHumanCompletion: true, placement: 'adjacent-to-primary', authority: 'none' }) : null, sharedRouting: null,
231
+ presentation: Object.freeze({ kind: 'bootstrap-only-carrier', label: 'Bootstrap carrier', authority: 'none', recipientLabel: '' }),
232
+ normalEmissionBoundary: Object.freeze({ allowed: Object.freeze(['package-file', 'generic-start-transport-text']), forbidden: Object.freeze(['workspace-label', 'route-specific-continue-from', 'recipient-label', 'holder-label', 'current-work-label']) }),
233
+ fallbackTransportText: ready ? Object.freeze({ supported: true, filename: filename.replace(/\.handoff-package\.zip$/i, '.transport.txt'), content: '', normalEmission: false, requiredForHumanCompletion: false, authority: 'none' }) : null, selectedRoute: null, findings: Object.freeze([]),
234
+ boundary: 'Bootstrap-only carrier output projection. Generic Start transport text only; no Workspace, Handoff route, recipient, holder, Role, or work projection exists.'
235
+ });
236
+ }
237
+
201
238
  function projectWorkspaceCarrierHumanOutput(result = {}, flags = {}) {
202
239
  const projection = result.carrierProjection || {};
203
240
  const ready = result.status === 'ready' && projection.status === 'ready' && projection.mode === 'workspace' && (projection.routes || []).length === 0;
@@ -213,11 +250,11 @@ function projectWorkspaceCarrierHumanOutput(result = {}, flags = {}) {
213
250
  schema: 'tiinex.portable.handoff-human-output.v1',
214
251
  status: ready ? 'ready' : 'blocked',
215
252
  primary: ready ? Object.freeze({ kind: 'workspace-package', filename, dimension, parentDimension: String(projection.lineage?.parentDimension || ''), checkpointKind: String(projection.lineage?.checkpointKind || ''), routeId: '', workspaceId: '', workspaceRelativeHandoffPath: '', collisionInstance: 1, singleHumanTransportChoice: true }) : null,
216
- normalInlineRouting: null, sharedRouting: null,
217
- presentation: Object.freeze({ kind: 'pointerless-workspace-carrier', label: 'Workspace carrier', authority: 'none' }),
218
- normalEmissionBoundary: Object.freeze({ allowed: Object.freeze(['package-file']), forbidden: Object.freeze(['handoff-routing-text', 'continue-from-pointer']) }),
219
- fallbackTransportText: null, selectedRoute: null, findings: Object.freeze([]),
220
- boundary: 'Pointerless Workspace-carrier output projection only. No Handoff routing text exists because the package role declares no Handoff route.'
253
+ normalInlineRouting: ready ? Object.freeze({ kind: 'transport-text', content: '', normalEmission: true, requiredForHumanCompletion: true, placement: 'adjacent-to-primary', authority: 'none' }) : null, sharedRouting: null,
254
+ presentation: Object.freeze({ kind: 'pointerless-workspace-carrier', label: 'Workspace carrier', authority: 'none', recipientLabel: '' }),
255
+ normalEmissionBoundary: Object.freeze({ allowed: Object.freeze(['package-file', 'generic-start-transport-text']), forbidden: Object.freeze(['route-specific-continue-from', 'recipient-label-from-material']) }),
256
+ fallbackTransportText: ready ? Object.freeze({ supported: true, filename: filename.replace(/\.handoff-package\.zip$/i, '.transport.txt'), content: '', normalEmission: false, requiredForHumanCompletion: false, authority: 'none' }) : null, selectedRoute: null, findings: Object.freeze([]),
257
+ boundary: 'Pointerless Workspace-carrier output projection. Generic Start transport text is permitted; Handoff Continue-from and recipient projection remain absent.'
221
258
  });
222
259
  }
223
260
 
@@ -0,0 +1,18 @@
1
+ import { buildToolingBootstrapTransportFiles } from './handoff.manufacture.bootstrap.js';
2
+ import { normalizeHandoffCarrierLineage } from '../../handoff/carrierLineage.js';
3
+ import { normalizeHandoffCarrierProfile } from '../../handoff/carrierProfile.js';
4
+
5
+ export async function prepareNodeBootstrapCarrierManufacturingInput(input = {}, options = {}) {
6
+ const toolingBootstrap = await buildToolingBootstrapTransportFiles({
7
+ delivery: input.toolingBootstrap || input.bootstrapDelivery || 'embedded',
8
+ runtimeRoot: input.runtimeRoot || options.runtimeRoot,
9
+ expected: input.expectedToolingBootstrap || null,
10
+ maxFiles: input.bootstrapMaxFiles || options.bootstrapMaxFiles
11
+ });
12
+ return Object.freeze({
13
+ carrierMode: 'bootstrap', createdAt: String(input.createdAt || ''), workspaceMaterializations: Object.freeze([]), workspaceTargets: Object.freeze([]),
14
+ additionalTransportFiles: toolingBootstrap.files,
15
+ carrierLineage: normalizeHandoffCarrierLineage(input.carrierLineage || null), carrierProfile: normalizeHandoffCarrierProfile(input.carrierProfile || null),
16
+ toolingBootstrap: toolingBootstrap.summary, manufacturingEvidence: Object.freeze({ toolingBootstrap: toolingBootstrap.summary }), verifyRoundtrip: input.verifyRoundtrip !== false
17
+ });
18
+ }