@tiinex/core 0.8.0 → 0.10.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 +5 -5
- package/src/lineage/parentRecoveryReference.js +49 -0
- package/src/schemas/creation.renderer.js +13 -2
- package/src/schemas/tiinex.root.v1.validate.js +9 -0
- package/src/tooling/portable/adapters/cli/cli.common-author.js +9 -1
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +2 -1
- package/src/tooling/portable/adapters/cli/cli.handoff-sibling-allocation.js +42 -18
- package/src/tooling/portable/adapters/cli/cli.help.js +1 -1
- package/src/tooling/portable/adapters/node/handoff.manufacture.requirements.js +1 -1
- package/src/tooling/portable/handoff/carrierProjection.routeQualification.js +20 -5
- package/src/tooling/portable/handoff/recipientV2.artifactFirst.closure.js +19 -2
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +1 -1
- package/src/tooling/portable/handoff/workspaceQualifiedReference.js +4 -6
- package/src/tooling/portable/handoff/workspaceTargetConformance.js +9 -5
- package/src/tooling/portable/output/recipientV2.zip.js +17 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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,
|
|
@@ -167,12 +167,12 @@
|
|
|
167
167
|
"type": "git",
|
|
168
168
|
"url": "git+https://github.com/Tiinex/core.git"
|
|
169
169
|
},
|
|
170
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "3a1efbac38c1cabc1a0d85746ccaab8149d6162e",
|
|
171
171
|
"tiinexRelease": {
|
|
172
172
|
"policy": "tiinex.master-npm-release.v1",
|
|
173
|
-
"sourceCommit": "
|
|
174
|
-
"sourceTree": "
|
|
173
|
+
"sourceCommit": "3a1efbac38c1cabc1a0d85746ccaab8149d6162e",
|
|
174
|
+
"sourceTree": "8c1449869841737fb7b120b69d00b4854cfff334",
|
|
175
175
|
"repository": "Tiinex/core",
|
|
176
|
-
"previousVersion": "0.
|
|
176
|
+
"previousVersion": "0.9.0"
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function parseWorkspaceQualifiedRecoveryReference(value = '') {
|
|
2
|
+
const rawValue = normalizedReferenceText(value);
|
|
3
|
+
const match = rawValue.match(/^([A-Za-z0-9._-]+)::(.+)$/);
|
|
4
|
+
if (!match) return null;
|
|
5
|
+
const rawPath = referencePathPart(match[2]);
|
|
6
|
+
if (!rawPath || rawPath.startsWith('/') || rawPath.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(rawPath)) return null;
|
|
7
|
+
const parts = rawPath.replace(/\\/g, '/').split('/');
|
|
8
|
+
if (parts.some((part) => !part || part === '..' || hasControlCharacters(part))) return null;
|
|
9
|
+
const path = parts.filter((part) => part !== '.').join('/');
|
|
10
|
+
return path ? Object.freeze({ workspaceId: match[1], path, reference: `${match[1]}::${path}` }) : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function classifyParentRecoveryReference(value = '') {
|
|
14
|
+
const raw = normalizedReferenceText(value);
|
|
15
|
+
if (!raw) return Object.freeze({ kind: 'empty', raw: '', workspaceQualified: null });
|
|
16
|
+
const workspaceQualified = parseWorkspaceQualifiedRecoveryReference(raw);
|
|
17
|
+
if (workspaceQualified) return Object.freeze({ kind: 'workspace-qualified', raw, workspaceQualified });
|
|
18
|
+
if (isNetworkOrSchemeReference(raw)) return Object.freeze({ kind: 'external', raw, workspaceQualified: null });
|
|
19
|
+
if (raw.includes('::')) return Object.freeze({ kind: 'malformed-workspace-qualified', raw, workspaceQualified: null });
|
|
20
|
+
return Object.freeze({ kind: 'local-relative', raw, workspaceQualified: null });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isMalformedWorkspaceQualifiedRecoveryReference(value = '') {
|
|
24
|
+
return classifyParentRecoveryReference(value).kind === 'malformed-workspace-qualified';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isExternalRecoveryReference(value = '') {
|
|
28
|
+
return classifyParentRecoveryReference(value).kind === 'external';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizedReferenceText(value = '') {
|
|
32
|
+
return String(value || '').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function referencePathPart(value = '') {
|
|
36
|
+
let decoded;
|
|
37
|
+
try { decoded = decodeURIComponent(String(value || '').split('#')[0].split('?')[0]); }
|
|
38
|
+
catch { return ''; }
|
|
39
|
+
return decoded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isNetworkOrSchemeReference(value = '') {
|
|
43
|
+
const text = String(value || '');
|
|
44
|
+
return /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(text) || text.startsWith('//') || (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(text) && !text.includes('::'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasControlCharacters(value = '') {
|
|
48
|
+
return /[\u0000-\u001f]/.test(String(value || ''));
|
|
49
|
+
}
|
|
@@ -3,6 +3,7 @@ import { schemaIdForRecord } from './schema.identity.js';
|
|
|
3
3
|
import { canonicalRootCreatedAt } from './creation.rootMetadata.js';
|
|
4
4
|
import { renderSchemaReference } from './schema.reference.js';
|
|
5
5
|
import { C14N_V2_METHOD_ID, renderIntegrityMethodReference } from '../integrity/integrity.methodReference.js';
|
|
6
|
+
import { parseWorkspaceQualifiedRecoveryReference } from '../lineage/parentRecoveryReference.js';
|
|
6
7
|
|
|
7
8
|
export const GENERIC_ARTIFACT_CREATION_RENDERER_ID = 'tiinex.site.generic-artifact-creation-renderer.v1';
|
|
8
9
|
|
|
@@ -194,11 +195,16 @@ function parentEnvelope(record = {}, childPath = '') {
|
|
|
194
195
|
const recoveryMode = normalizeParentRecoveryMode(record.recoveryMode || record.parentRecoveryMode || 'local-relative');
|
|
195
196
|
const published = normalizePublishedReference(record.publishedReference || record.browseGitReference || record.browseGit || '');
|
|
196
197
|
const publishedReference = published.state === 'qualified' ? published.target : '';
|
|
197
|
-
const relativeReference = recoveryMode === 'external-versioned'
|
|
198
|
+
const relativeReference = recoveryMode === 'external-versioned'
|
|
199
|
+
? ''
|
|
200
|
+
: recoveryMode === 'workspace-qualified'
|
|
201
|
+
? String(record.relativeReference || parentPath).trim()
|
|
202
|
+
: String(record.relativeReference || relativePath(dirname(child), parentPath)).trim();
|
|
198
203
|
const traceReference = recoveryMode === 'external-versioned' ? publishedReference : relativeReference;
|
|
199
204
|
const schemaReferenceAuthority = normalizeParentSchemaReferenceAuthority(record.schemaReferenceAuthority || record.parentSchemaReferenceAuthority, schemaId);
|
|
200
205
|
if (!schemaId || !parentPath || !child || !traceReference) throw new Error('creation-parent-identity-incomplete');
|
|
201
206
|
if (recoveryMode === 'external-versioned' && !publishedReference) throw new Error('creation-parent-external-versioned-reference-required');
|
|
207
|
+
if (recoveryMode === 'workspace-qualified' && !parseWorkspaceQualifiedRecoveryReference(relativeReference)) throw new Error('creation-parent-workspace-qualified-reference-invalid');
|
|
202
208
|
const parentSelf = validatedC14nV2PrimarySelfDigest(record.markdown || '');
|
|
203
209
|
if (parentSelf.state !== 'verified') throw new Error(`creation-parent-primary-self-${parentSelf.reason || parentSelf.state}`);
|
|
204
210
|
const integrityTarget = publishedReference || relativeReference;
|
|
@@ -217,7 +223,12 @@ function parentEnvelope(record = {}, childPath = '') {
|
|
|
217
223
|
});
|
|
218
224
|
}
|
|
219
225
|
|
|
220
|
-
function normalizeParentRecoveryMode(value = '') {
|
|
226
|
+
function normalizeParentRecoveryMode(value = '') {
|
|
227
|
+
const mode = String(value || '').trim();
|
|
228
|
+
if (mode === 'external-versioned') return 'external-versioned';
|
|
229
|
+
if (mode === 'workspace-qualified') return 'workspace-qualified';
|
|
230
|
+
return 'local-relative';
|
|
231
|
+
}
|
|
221
232
|
function normalizePublishedReference(value) {
|
|
222
233
|
if (typeof value === 'string') return Object.freeze({ target: value, state: value ? 'unresolved' : 'unavailable' });
|
|
223
234
|
return Object.freeze({ target: String(value?.target || value?.url || ''), state: String(value?.state || value?.resolutionState || 'unresolved') });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isMalformedWorkspaceQualifiedRecoveryReference } from '../lineage/parentRecoveryReference.js';
|
|
2
|
+
|
|
1
3
|
export function rootValidate(artifact) {
|
|
2
4
|
const findings = [];
|
|
3
5
|
const envelope = artifact?.envelope || {};
|
|
@@ -20,6 +22,13 @@ export function rootValidate(artifact) {
|
|
|
20
22
|
const labels = originEntries.map((entry) => String(entry?.label || '').trim()).filter(Boolean);
|
|
21
23
|
const duplicateLabels = [...new Set(labels.filter((label, index) => labels.indexOf(label) !== index))];
|
|
22
24
|
for (const label of duplicateLabels) findings.push(error('root.parent.origin.label.duplicate', `Parent Origin recovery label is duplicated: ${label}.`));
|
|
25
|
+
const recoveryReferences = [
|
|
26
|
+
Object.freeze({ field: 'Trace', target: String(parent.trace || '').trim() }),
|
|
27
|
+
...originEntries.map((entry) => Object.freeze({ field: `Origin:${String(entry?.label || '').trim() || 'unlabelled'}`, target: String(entry?.target || '').trim() }))
|
|
28
|
+
].filter((entry) => entry.target);
|
|
29
|
+
for (const reference of recoveryReferences) {
|
|
30
|
+
if (isMalformedWorkspaceQualifiedRecoveryReference(reference.target)) findings.push(error('root.parent.recovery.workspace-qualified.malformed', `Parent ${reference.field} contains a malformed Workspace-qualified recovery locator: ${reference.target}.`));
|
|
31
|
+
}
|
|
23
32
|
}
|
|
24
33
|
if (envelope.repairsDeclared) findings.push(info('root.repairs.declared', 'Envelope declares repair notes; validators should preserve unknown repair fields.'));
|
|
25
34
|
if (!findings.some((finding) => finding.severity === 'error')) findings.push(info('root.envelope.readable', 'Root envelope is readable at current validation depth.'));
|
|
@@ -9,6 +9,7 @@ import { resolveSchemaModule } from '../../../../schemas/resolver.js';
|
|
|
9
9
|
import { loadNodePortableInput } from '../../input/node.input.js';
|
|
10
10
|
import { runPortableOperation } from '../../operation.catalog.js';
|
|
11
11
|
import { allocateContinuationPath, allocateDirectoryArtifactPath } from '../../../../transitions/record.transitions.js';
|
|
12
|
+
import { classifyParentRecoveryReference } from '../../../../lineage/parentRecoveryReference.js';
|
|
12
13
|
|
|
13
14
|
const STATE_RELATIVE_PATH = '.tiinex/continuation.json';
|
|
14
15
|
|
|
@@ -137,11 +138,18 @@ async function parentRecordFromArtifact(parentPath, parentRelativePath, context
|
|
|
137
138
|
currentCreatedAt: String(current.createdAt || ''),
|
|
138
139
|
createdAt: String(current.createdAt || ''),
|
|
139
140
|
markdown,
|
|
140
|
-
recoveryMode:
|
|
141
|
+
recoveryMode: parentRecoveryMode(parentRelativePath),
|
|
141
142
|
schemaReferenceAuthority
|
|
142
143
|
});
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
export function parentRecoveryMode(reference = '') {
|
|
147
|
+
const classification = classifyParentRecoveryReference(reference);
|
|
148
|
+
if (classification.kind === 'workspace-qualified') return 'workspace-qualified';
|
|
149
|
+
if (classification.kind === 'malformed-workspace-qualified') throw new Error('portable.cli.author.parent.workspace-qualified.malformed');
|
|
150
|
+
return 'local-relative';
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
function exactDeclaredSchemaReferenceAuthority(schemaId, schemaTarget) {
|
|
146
154
|
return Object.freeze({
|
|
147
155
|
schemaId,
|
|
@@ -85,7 +85,8 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
85
85
|
parentPackagePath: resolvedParent,
|
|
86
86
|
parentPackageSha256: provisionalLineage.parentPackageSha256,
|
|
87
87
|
parentDimension: provisionalLineage.parentDimension,
|
|
88
|
-
enabled: !flags['package-major'] && Boolean(flags.output || flags['output-dir'])
|
|
88
|
+
enabled: !flags['package-major'] && Boolean(flags.output || flags['output-dir']),
|
|
89
|
+
siblingIndex: flags['package-sibling-index']
|
|
89
90
|
});
|
|
90
91
|
carrierLineage = flags['package-major'] ? provisionalLineage : carrierLineageFromCliParent({
|
|
91
92
|
bundle: parentBundle,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { mkdir, open } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
const MAX_SIBLING_INDEX = 9999;
|
|
5
5
|
|
|
6
|
-
export async function reserveHandoffSiblingIndex({ parentPackagePath = '', parentPackageSha256 = '', parentDimension = '', enabled = true } = {}) {
|
|
7
|
-
|
|
6
|
+
export async function reserveHandoffSiblingIndex({ parentPackagePath = '', parentPackageSha256 = '', parentDimension = '', enabled = true, siblingIndex = null } = {}) {
|
|
7
|
+
const requestedSiblingIndex = normalizeSiblingIndex(siblingIndex);
|
|
8
|
+
if (!enabled) return Object.freeze({ state: 'not-reserved', siblingIndex: requestedSiblingIndex || 1, allocationPath: '', coordinationRequired: Boolean(requestedSiblingIndex) });
|
|
9
|
+
if (!requestedSiblingIndex) throw new Error('portable.cli.handoff-carrier.sibling-allocation.explicit-index-required');
|
|
8
10
|
const parentPath = path.resolve(String(parentPackagePath || ''));
|
|
9
11
|
const digest = String(parentPackageSha256 || '').trim().toLowerCase();
|
|
10
12
|
const dimension = String(parentDimension || '').trim();
|
|
@@ -12,20 +14,42 @@ export async function reserveHandoffSiblingIndex({ parentPackagePath = '', paren
|
|
|
12
14
|
const allocationDir = path.join(path.dirname(parentPath), '.tiinex-handoff-sibling-allocations');
|
|
13
15
|
await mkdir(allocationDir, { recursive: true });
|
|
14
16
|
const key = `${digest.slice(0, 24)}-${dimension.replace(/[^0-9-]/g, '')}`;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
const allocationPath = path.join(allocationDir, `${key}-${requestedSiblingIndex}.allocation`);
|
|
18
|
+
const record = Object.freeze({
|
|
19
|
+
parentPackageSha256: digest,
|
|
20
|
+
parentDimension: dimension,
|
|
21
|
+
siblingIndex: requestedSiblingIndex,
|
|
22
|
+
childDimension: `${dimension}-${requestedSiblingIndex}`,
|
|
23
|
+
allocationAuthority: 'explicit-caller-coordination'
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const handle = await open(allocationPath, 'wx');
|
|
27
|
+
try { await handle.writeFile(`${JSON.stringify(record)}\n`, 'utf8'); }
|
|
28
|
+
finally { await handle.close(); }
|
|
29
|
+
return Object.freeze({ state: 'reserved-explicit', siblingIndex: requestedSiblingIndex, allocationPath, coordinationRequired: true });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
32
|
+
const prior = await readAllocation(allocationPath);
|
|
33
|
+
if (!sameAllocation(prior, record)) throw new Error('portable.cli.handoff-carrier.sibling-allocation.existing-reservation-conflict');
|
|
34
|
+
return Object.freeze({ state: 'reused-explicit', siblingIndex: requestedSiblingIndex, allocationPath, coordinationRequired: true });
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeSiblingIndex(value) {
|
|
39
|
+
if (value === null || value === undefined || value === '') return 0;
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_SIBLING_INDEX) throw new Error('portable.cli.handoff-carrier.sibling-allocation.index-invalid');
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readAllocation(allocationPath) {
|
|
46
|
+
try { return JSON.parse(await readFile(allocationPath, 'utf8')); }
|
|
47
|
+
catch { throw new Error('portable.cli.handoff-carrier.sibling-allocation.existing-reservation-invalid'); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sameAllocation(left = {}, right = {}) {
|
|
51
|
+
return String(left.parentPackageSha256 || '') === String(right.parentPackageSha256 || '')
|
|
52
|
+
&& String(left.parentDimension || '') === String(right.parentDimension || '')
|
|
53
|
+
&& Number(left.siblingIndex || 0) === Number(right.siblingIndex || 0)
|
|
54
|
+
&& String(left.childDimension || '') === String(right.childDimension || '');
|
|
31
55
|
}
|
|
@@ -70,7 +70,7 @@ function commonCommandHelp(command, surfaceCommand) {
|
|
|
70
70
|
'',
|
|
71
71
|
`${command} handoff <workspace-dir>`,
|
|
72
72
|
'',
|
|
73
|
-
'Infers the latest qualified authored Handoff, selected Workspace identity/target, received package parent as carrier-lineage evidence, canonical projected filename, and return output directory. It does not implicitly carry sibling Workspaces from the received package; advanced manufacture must select exact reusable parent snapshots with `--package-parent-workspaces <id,...|all>`. The default receipt keeps output identity, routing text, closure/workspace qualification, verification, and actionable findings compact; add `--full` for the complete manufacture receipt. Normal operator completion is exactly one Handoff package plus the adjacent exact routing text. In markdown-capable hosts render that routing in a fenced code block; do not emit canonical Workspace Evidence/Handoff markdown as additional loose transport files. Runtime-only `.tiinex` state is excluded from canonical manufacture.',
|
|
73
|
+
'Infers the latest qualified authored Handoff, selected Workspace identity/target, received package parent as carrier-lineage evidence, canonical projected filename, and return output directory. A non-major continuation that writes a carrier must supply an explicitly coordinated `--package-sibling-index <1..9999>`; Tooling will not silently discover the next sibling because isolated runtimes cannot prove a globally unique reservation. Distinct explicit sibling indexes preserve same-Major parallelism, while byte-identical duplicate transport at one exact output path is idempotent and divergent bytes fail closed. It does not implicitly carry sibling Workspaces from the received package; advanced manufacture must select exact reusable parent snapshots with `--package-parent-workspaces <id,...|all>`. The default receipt keeps output identity, routing text, closure/workspace qualification, verification, and actionable findings compact; add `--full` for the complete manufacture receipt. Normal operator completion is exactly one Handoff package plus the adjacent exact routing text. In markdown-capable hosts render that routing in a fenced code block; do not emit canonical Workspace Evidence/Handoff markdown as additional loose transport files. Runtime-only `.tiinex` state is excluded from canonical manufacture.',
|
|
74
74
|
'',
|
|
75
75
|
`Advanced/internal catalog: ${command} operations`
|
|
76
76
|
];
|
|
@@ -273,7 +273,7 @@ function resolveCarriedWorkspaceQualifiedReference(target = '', workspaceRuntime
|
|
|
273
273
|
|
|
274
274
|
export function resolveRelativeWorkspaceTarget(sourcePath = '', target = '') {
|
|
275
275
|
const raw = safeDecodeURIComponent(String(target || '').split('#')[0].split('?')[0]);
|
|
276
|
-
if (!raw || path.posix.isAbsolute(raw) || raw.startsWith('\\')) return '';
|
|
276
|
+
if (!raw || path.posix.isAbsolute(raw) || raw.startsWith('\\') || raw.includes('::')) return '';
|
|
277
277
|
const base = normalizeRelativePath(sourcePath).split('/').slice(0, -1);
|
|
278
278
|
for (const part of raw.replace(/\\/g, '/').split('/')) {
|
|
279
279
|
if (!part || part === '.') continue;
|
|
@@ -4,6 +4,7 @@ import { projectHandoffMaterialRequirements, projectParticipantRoleRequirements
|
|
|
4
4
|
import { qualifySelectedHandoffArtifact } from './routeArtifactConformance.js';
|
|
5
5
|
import { listHandoffWorkspaceEntries, resolveHandoffWorkspaceEntry } from './workspaceByteProvider.js';
|
|
6
6
|
import { parseWorkspaceQualifiedReference, SHARED_ROUTE_REQUIRED_CONTEXT_BOUNDARY } from './workspaceQualifiedReference.js';
|
|
7
|
+
import { classifyParentRecoveryReference } from '../../../lineage/parentRecoveryReference.js';
|
|
7
8
|
import { decodeUtf8, deepFreeze, findFile, normalizeWorkspacePath } from './carrierProjection.shared.js';
|
|
8
9
|
|
|
9
10
|
export function qualifyRoute(bundle, descriptor, byteProvider, workspace, spec = {}, options = {}) {
|
|
@@ -132,13 +133,27 @@ function resolveDescriptorMaterial(bundle, descriptor, byteProvider, target = ''
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
function resolveRouteParent(bundle, descriptor, byteProvider, workspace, routePath, parent = {}, targetEntry = {}) {
|
|
135
|
-
const
|
|
136
|
-
if (parent.trace
|
|
136
|
+
const recoveryTargets = [];
|
|
137
|
+
if (parent.trace) recoveryTargets.push(String(parent.trace));
|
|
137
138
|
for (const entry of parent.originEntries || []) {
|
|
138
|
-
if (String(entry?.label || '').trim() === 'relative' && entry?.target
|
|
139
|
+
if (String(entry?.label || '').trim() === 'relative' && entry?.target) recoveryTargets.push(String(entry.target));
|
|
139
140
|
}
|
|
140
141
|
const candidates = new Map();
|
|
141
|
-
for (const target of
|
|
142
|
+
for (const target of [...new Set(recoveryTargets)]) {
|
|
143
|
+
const classification = classifyParentRecoveryReference(target);
|
|
144
|
+
if (classification.kind === 'malformed-workspace-qualified') return Object.freeze({ state: 'unavailable', reason: 'parent-workspace-qualified-reference-malformed' });
|
|
145
|
+
if (classification.kind === 'external' || classification.kind === 'empty') continue;
|
|
146
|
+
if (classification.kind === 'workspace-qualified') {
|
|
147
|
+
const qualified = classification.workspaceQualified;
|
|
148
|
+
const resolved = resolveHandoffWorkspaceEntry(byteProvider, qualified.workspaceId, qualified.path);
|
|
149
|
+
if (resolved.state !== 'qualified') continue;
|
|
150
|
+
const data = packageFileBytes({ data: resolved.data });
|
|
151
|
+
if (Number(resolved.bytes || 0) !== data.byteLength || String(resolved.sha256 || '') !== sha256Hex(data)) continue;
|
|
152
|
+
const markdown = decodeUtf8(data);
|
|
153
|
+
if (!markdown) continue;
|
|
154
|
+
candidates.set(`${qualified.workspaceId}\u0000${qualified.path}`, Object.freeze({ state: 'qualified', markdown, basis: 'parent-workspace-qualified-reference', workspaceId: qualified.workspaceId, workspaceRelativePath: qualified.path, packagePath: String(resolved.packagePath || ''), sha256: sha256Hex(data) }));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
142
157
|
const resolvedPath = resolveWorkspaceReference(routePath, target);
|
|
143
158
|
if (!resolvedPath) continue;
|
|
144
159
|
const resolved = resolveHandoffWorkspaceEntry(byteProvider, workspace.id, resolvedPath);
|
|
@@ -192,7 +207,7 @@ function packageParentCandidates(bundle = {}, descriptor = {}, byteProvider = {}
|
|
|
192
207
|
|
|
193
208
|
function resolveWorkspaceReference(routePath, target) {
|
|
194
209
|
const raw = safeDecodeURIComponent(String(target || '').split('#')[0].split('?')[0]);
|
|
195
|
-
if (!raw || raw.startsWith('/') || raw.startsWith('\\')) return '';
|
|
210
|
+
if (!raw || raw.startsWith('/') || raw.startsWith('\\') || raw.includes('::')) return '';
|
|
196
211
|
const base = normalizeWorkspacePath(routePath).split('/').slice(0, -1);
|
|
197
212
|
for (const part of raw.replace(/\\/g, '/').split('/')) {
|
|
198
213
|
if (!part || part === '.') continue;
|
|
@@ -3,6 +3,7 @@ import { validatedC14nV2PrimarySelfDigest } from '../../../integrity/integrity.c
|
|
|
3
3
|
import { projectHandoffMaterialRequirements } from './materialClosure.requirements.js';
|
|
4
4
|
import { deepFreeze, finding, normalizeRoutePath, sectionText, fieldValue, decodeUtf8 } from './recipientV2.artifactFirst.shared.js';
|
|
5
5
|
import { parseWorkspaceQualifiedReference } from './workspaceQualifiedReference.js';
|
|
6
|
+
import { classifyParentRecoveryReference } from '../../../lineage/parentRecoveryReference.js';
|
|
6
7
|
|
|
7
8
|
export function qualifyRecipientV2ArtifactFirstPhase1RequiredContextClosure(input = {}) {
|
|
8
9
|
return qualifyPhase1RequiredContextClosure(input);
|
|
@@ -71,7 +72,23 @@ export function qualifyPhase1RequiredContextClosure({ markdown = '', routePath =
|
|
|
71
72
|
export function resolveArchiveParent(routePath = '', entries = [], parent = {}, targetEntry = {}, parentCandidates = []) {
|
|
72
73
|
const refs = [String(parent.trace || ''), ...(parent.originEntries || []).map((item) => String(item.target || '')), String(targetEntry.towards || '')].filter(Boolean);
|
|
73
74
|
for (const ref of refs) {
|
|
74
|
-
|
|
75
|
+
const classification = classifyParentRecoveryReference(ref);
|
|
76
|
+
if (classification.kind === 'malformed-workspace-qualified') return Object.freeze({ state: 'unresolved', reason: 'parent-workspace-qualified-reference-malformed' });
|
|
77
|
+
if (classification.kind === 'external' || classification.kind === 'empty') continue;
|
|
78
|
+
if (classification.kind === 'workspace-qualified') {
|
|
79
|
+
const qualified = classification.workspaceQualified;
|
|
80
|
+
const matches = (parentCandidates || []).filter((candidate) => String(candidate.workspaceId || '') === qualified.workspaceId && String(candidate.workspaceRelativePath || '') === qualified.path);
|
|
81
|
+
if (matches.length > 1) return Object.freeze({ state: 'ambiguous', reason: 'multiple-parent-workspace-qualified-reference-candidates' });
|
|
82
|
+
if (matches.length === 1) {
|
|
83
|
+
const candidate = matches[0];
|
|
84
|
+
const data = packageFileBytes({ data: candidate.data });
|
|
85
|
+
if (data.byteLength && Number(candidate.bytes || data.byteLength) === data.byteLength && (!candidate.sha256 || String(candidate.sha256) === sha256Hex(data))) {
|
|
86
|
+
const markdown = decodeUtf8(data);
|
|
87
|
+
if (markdown) return Object.freeze({ state: 'qualified', markdown, basis: 'artifact-first-workspace-qualified-reference', workspaceId: qualified.workspaceId, workspaceRelativePath: qualified.path, archiveEntry: String(candidate.archiveEntry || ''), sha256: sha256Hex(data) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
75
92
|
const resolved = resolveRelativeWorkspacePath(routePath, ref);
|
|
76
93
|
if (!resolved) continue;
|
|
77
94
|
const matches = entries.filter((entry) => String(entry.path || '') === resolved);
|
|
@@ -115,7 +132,7 @@ export function phase1CacheParentCandidates(cacheQualifications = []) {
|
|
|
115
132
|
|
|
116
133
|
export function resolveRelativeWorkspacePath(fromPath = '', ref = '') {
|
|
117
134
|
const value = String(ref || '').split('#')[0].replace(/\\/g, '/');
|
|
118
|
-
if (!value || value.startsWith('/') || /^[A-Za-z]:\//.test(value)) return '';
|
|
135
|
+
if (!value || value.startsWith('/') || /^[A-Za-z]:\//.test(value) || value.includes('::')) return '';
|
|
119
136
|
const base = String(fromPath || '').replace(/\\/g, '/').split('/'); base.pop();
|
|
120
137
|
const parts = value.startsWith('./') || value.startsWith('../') ? [...base, ...value.split('/')] : [...base, ...value.split('/')];
|
|
121
138
|
const out = [];
|
|
@@ -284,7 +284,7 @@ function qualifyParentBoundaryCacheRequirementIds(caches = [], workspaceByteProv
|
|
|
284
284
|
function resolveRelativeWorkspacePath(sourcePath = '', target = '') {
|
|
285
285
|
let raw;
|
|
286
286
|
try { raw = decodeURIComponent(String(target || '').split('#')[0].split('?')[0]); } catch { return ''; }
|
|
287
|
-
if (!raw || raw.startsWith('/') || raw.startsWith('\\') || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith('//')) return '';
|
|
287
|
+
if (!raw || raw.startsWith('/') || raw.startsWith('\\') || raw.includes('::') || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith('//')) return '';
|
|
288
288
|
const parts = normalizeWorkspacePath(sourcePath).split('/').slice(0, -1);
|
|
289
289
|
for (const part of raw.replace(/\\/g, '/').split('/')) {
|
|
290
290
|
if (!part || part === '.') continue;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { parseWorkspaceQualifiedRecoveryReference } from '../../../lineage/parentRecoveryReference.js';
|
|
2
|
+
|
|
1
3
|
export const SHARED_ROUTE_REQUIRED_CONTEXT_BOUNDARY = 'Shared-route recipient grounding proof only. Every Required Context item must resolve to exact carried package bytes; Reference Context is intentionally excluded from this blocking projection.';
|
|
2
4
|
|
|
3
5
|
export function parseWorkspaceQualifiedReference(value = '') {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const raw = String(match[2] || '').split('#')[0].split('?')[0].replace(/\\/g, '/');
|
|
7
|
-
if (!raw || raw.startsWith('/') || raw.split('/').some((part) => part === '..')) return null;
|
|
8
|
-
const path = raw.split('/').filter((part) => part && part !== '.').join('/');
|
|
9
|
-
return path ? Object.freeze({ workspaceId: match[1], path }) : null;
|
|
6
|
+
const parsed = parseWorkspaceQualifiedRecoveryReference(value);
|
|
7
|
+
return parsed ? Object.freeze({ workspaceId: parsed.workspaceId, path: parsed.path }) : null;
|
|
10
8
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validatedC14nV2PrimarySelfDigest } from '../../../integrity/integrity.c14nV2.js';
|
|
2
2
|
import { qualifyTiinexRouteArtifact } from './routeArtifactConformance.js';
|
|
3
|
+
import { classifyParentRecoveryReference } from '../../../lineage/parentRecoveryReference.js';
|
|
3
4
|
|
|
4
5
|
export const HANDOFF_WORKSPACE_TARGET_CONFORMANCE_SCHEMA_ID = 'tiinex.portable.handoff-workspace-target-conformance.v1';
|
|
5
6
|
|
|
@@ -48,7 +49,10 @@ function resolveWorkspaceParent({ entries = [], parentCandidates = [], targetPat
|
|
|
48
49
|
const indexed = indexEntries(entries);
|
|
49
50
|
const recoveryIndexed = indexEntries(parentCandidates);
|
|
50
51
|
const localCandidates = new Map();
|
|
51
|
-
for (const reference of
|
|
52
|
+
for (const reference of parentRecoveryReferences(parent)) {
|
|
53
|
+
const classification = classifyParentRecoveryReference(reference);
|
|
54
|
+
if (classification.kind === 'malformed-workspace-qualified') return Object.freeze({ state: 'unavailable', reason: 'parent-workspace-qualified-reference-malformed' });
|
|
55
|
+
if (classification.kind !== 'local-relative') continue;
|
|
52
56
|
const resolvedPath = resolveRelativeReference(targetPath, reference);
|
|
53
57
|
if (!resolvedPath) continue;
|
|
54
58
|
const entry = indexed.get(resolvedPath);
|
|
@@ -87,18 +91,18 @@ function indexEntries(entries = []) {
|
|
|
87
91
|
return out;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
function
|
|
94
|
+
function parentRecoveryReferences(parent = {}) {
|
|
91
95
|
const out = [];
|
|
92
|
-
if (parent.trace
|
|
96
|
+
if (parent.trace) out.push(String(parent.trace));
|
|
93
97
|
for (const entry of parent.originEntries || []) {
|
|
94
|
-
if (String(entry?.label || '').trim() === 'relative' && entry?.target
|
|
98
|
+
if (String(entry?.label || '').trim() === 'relative' && entry?.target) out.push(String(entry.target));
|
|
95
99
|
}
|
|
96
100
|
return [...new Set(out)];
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function resolveRelativeReference(fromPath = '', reference = '') {
|
|
100
104
|
const base = normalizeInnerPath(fromPath);
|
|
101
|
-
if (!base || !reference || isExternalReference(reference)) return '';
|
|
105
|
+
if (!base || !reference || isExternalReference(reference) || String(reference).includes('::')) return '';
|
|
102
106
|
let clean;
|
|
103
107
|
try { clean = decodeURIComponent(String(reference).split('#')[0].split('?')[0]); } catch { return ''; }
|
|
104
108
|
if (!clean || clean.startsWith('/') || clean.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(clean)) return '';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { packageFileBytes } from '../../../export/package.bytes.js';
|
|
5
5
|
import { inspectRecipientFacingV2Topology } from '../handoff/recipientV2.inspect.js';
|
|
@@ -38,10 +38,10 @@ export async function writeRecipientFacingV2PackageZip(bundle = {}, outputPath =
|
|
|
38
38
|
if (!target.toLowerCase().endsWith('.zip')) throw new Error('portable.recipient-v2.zip.output.extension');
|
|
39
39
|
await mkdir(path.dirname(target), { recursive: true });
|
|
40
40
|
const buffer = recipientFacingV2PackageZipBuffer(bundle, options);
|
|
41
|
-
await
|
|
41
|
+
const exactWrite = await writeExactRecipientTransportBytes(target, buffer);
|
|
42
42
|
return Object.freeze({
|
|
43
43
|
schema: 'tiinex.portable.recipient-facing-v2.zip-write.v1',
|
|
44
|
-
status:
|
|
44
|
+
status: exactWrite.status,
|
|
45
45
|
path: target,
|
|
46
46
|
bytes: buffer.length,
|
|
47
47
|
transportFormat: RECIPIENT_V2_FORMAT_ID,
|
|
@@ -49,6 +49,20 @@ export async function writeRecipientFacingV2PackageZip(bundle = {}, outputPath =
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export async function writeExactRecipientTransportBytes(outputPath = '', data = new Uint8Array()) {
|
|
53
|
+
const target = path.resolve(String(outputPath || '').trim());
|
|
54
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
55
|
+
try {
|
|
56
|
+
await writeFile(target, buffer, { flag: 'wx' });
|
|
57
|
+
return Object.freeze({ status: 'written', path: target, bytes: buffer.length });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
60
|
+
const existing = await readFile(target);
|
|
61
|
+
if (!existing.equals(buffer)) throw new Error('portable.recipient-v2.zip.output-collision-divergent');
|
|
62
|
+
return Object.freeze({ status: 'reused-identical', path: target, bytes: buffer.length });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
function bufferViewOfPackageFile(file = {}) {
|
|
53
67
|
const value = file.data;
|
|
54
68
|
if (Buffer.isBuffer(value)) return value;
|