@tiinex/core 0.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiinex/core",
3
- "version": "0.14.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": "030669b6596196da946edb049d15af044bcd96c6",
171
+ "gitHead": "d257bb3ae9e7c6361840d727ce5e359d1b6f6091",
172
172
  "tiinexRelease": {
173
173
  "policy": "tiinex.master-npm-release.v1",
174
- "sourceCommit": "030669b6596196da946edb049d15af044bcd96c6",
175
- "sourceTree": "090aaead28df51b2aeb6679018526f54260a1ca8",
174
+ "sourceCommit": "d257bb3ae9e7c6361840d727ce5e359d1b6f6091",
175
+ "sourceTree": "002c94ff43122ac8188de9514909dfdabd348803",
176
176
  "repository": "Tiinex/core",
177
- "previousVersion": "0.13.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);