@tiinex/core 0.15.0 → 0.16.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 +7 -6
- package/src/tooling/portable/adapters/cli/cli.command-input.js +11 -0
- package/src/tooling/portable/adapters/cli/cli.ground-materialize.js +2 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +4 -1
- package/src/tooling/portable/adapters/cli/cli.help.js +1 -1
- package/src/tooling/portable/adapters/cli/cli.land.js +23 -1
- package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +9 -1
- package/src/tooling/portable/handoff/carrierProjection.js +2 -2
- package/src/tooling/portable/handoff/carrierProjection.routeQualification.js +1 -0
- package/src/tooling/portable/handoff/delegationReturnReservation.js +101 -0
- package/src/tooling/portable/handoff/manufacture.js +7 -1
- package/src/tooling/portable/handoff/materialClosure.archiveV2.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.artifactInspection.js +11 -0
- package/src/tooling/portable/handoff/recipientV2.inspect.projection.js +7 -3
- package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +3 -2
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +7 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +15 -2
- package/src/tooling/portable/handoff/recipientV2.pointer.js +2 -0
- package/src/tooling/portable/handoff/recoveryAcceptanceAudit.js +145 -0
- package/src/tooling/portable/handoff/workspaceLandingPlan.js +110 -13
- package/src/tooling/portable/operation.catalog.package.js +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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,
|
|
@@ -136,7 +136,8 @@
|
|
|
136
136
|
"./node/release-policy": "./src/release/policy.mjs",
|
|
137
137
|
"./tooling/portable/source/sourceEligibility.js": "./src/tooling/portable/source/sourceEligibility.js",
|
|
138
138
|
"./tooling/portable/comparison/sourceFrontierComparison.js": "./src/tooling/portable/comparison/sourceFrontierComparison.js",
|
|
139
|
-
"./tooling/portable/adapters/node/sourceFrontierComparison.js": "./src/tooling/portable/adapters/node/sourceFrontierComparison.js"
|
|
139
|
+
"./tooling/portable/adapters/node/sourceFrontierComparison.js": "./src/tooling/portable/adapters/node/sourceFrontierComparison.js",
|
|
140
|
+
"./tooling/portable/handoff/recoveryAcceptanceAudit.js": "./src/tooling/portable/handoff/recoveryAcceptanceAudit.js"
|
|
140
141
|
},
|
|
141
142
|
"files": [
|
|
142
143
|
"src",
|
|
@@ -168,12 +169,12 @@
|
|
|
168
169
|
"type": "git",
|
|
169
170
|
"url": "git+https://github.com/Tiinex/core.git"
|
|
170
171
|
},
|
|
171
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "4da833bc6d123ea2b37acc9bc634c097afa0a91d",
|
|
172
173
|
"tiinexRelease": {
|
|
173
174
|
"policy": "tiinex.master-npm-release.v1",
|
|
174
|
-
"sourceCommit": "
|
|
175
|
-
"sourceTree": "
|
|
175
|
+
"sourceCommit": "4da833bc6d123ea2b37acc9bc634c097afa0a91d",
|
|
176
|
+
"sourceTree": "4143fd68b602b898bcdc18383b506f28a560a3ce",
|
|
176
177
|
"repository": "Tiinex/core",
|
|
177
|
-
"previousVersion": "0.
|
|
178
|
+
"previousVersion": "0.15.0"
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -141,6 +141,17 @@ export async function commandInput(parsed, runtime = {}) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
if (parsed.command === 'compare-source-frontiers') return prepareSourceFrontierComparisonCliInput(parsed, flags);
|
|
144
|
+
if (parsed.command === 'audit-recovery-acceptance') {
|
|
145
|
+
const basisPath = String(flags.basis || parsed.positionals[0] || '').trim();
|
|
146
|
+
const candidatePath = String(flags.candidate || parsed.positionals[1] || '').trim();
|
|
147
|
+
if (!basisPath || !candidatePath) throw new Error('portable.cli.recovery-acceptance.basis-candidate-required');
|
|
148
|
+
const [basis, candidate, expectedValue] = await Promise.all([
|
|
149
|
+
loadNodePortableInput([basisPath], { maxFiles: flags['max-carrier-files'] || 10000, maxTextBytes: flags['max-text-bytes'] || 16 * 1024 * 1024 }),
|
|
150
|
+
loadNodePortableInput([candidatePath], { maxFiles: flags['max-carrier-files'] || 10000, maxTextBytes: flags['max-text-bytes'] || 16 * 1024 * 1024 }),
|
|
151
|
+
readOptionalJson(flags['expected-removals'])
|
|
152
|
+
]);
|
|
153
|
+
return { input: { basis, candidate, workspaceIds: splitFlag(flags.workspaces || flags['workspace-ids']), expectedRemovals: expectedValue.expectedRemovals || expectedValue || {} }, options: {} };
|
|
154
|
+
}
|
|
144
155
|
if (parsed.command === 'prove-source-reconciliation') {
|
|
145
156
|
const dispositions = await readOptionalJson(flags.dispositions || flags['disposition-file']);
|
|
146
157
|
return prepareSourceFrontierReconciliationCliInput(flags, dispositions);
|
|
@@ -50,6 +50,8 @@ export async function materializeGroundWorkspaceCliOutput(result = {}, input = {
|
|
|
50
50
|
workspaceTarget: String(workspaceInspection.sourceWorkspaceTargetInnerPath || ''),
|
|
51
51
|
roleLabel: String(result?.authority?.role?.label || ''),
|
|
52
52
|
returnOutputDir: path.dirname(path.resolve(String(input.packageSourcePath || outputDir))),
|
|
53
|
+
returnPackageCarrierKind: String((inspection.routes || []).find((item) => String(item.pointerPath || '') === String(result?.authority?.route?.pointerPath || input.route || ''))?.returnCarrierReservation?.carrierKind || ''),
|
|
54
|
+
returnPackageSiblingIndex: String((inspection.routes || []).find((item) => String(item.pointerPath || '') === String(result?.authority?.route?.pointerPath || input.route || ''))?.returnCarrierReservation?.siblingIndex || ''),
|
|
53
55
|
boundary: 'Runtime-only continuation state carried forward from one qualified ground --continue receipt. It is excluded from canonical Workspace manufacture and is not semantic authority.'
|
|
54
56
|
});
|
|
55
57
|
const continuationStatePath = path.join(outputDir, '.tiinex', 'continuation.json');
|
|
@@ -26,6 +26,7 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
26
26
|
const handoffPath = flags.handoff || parsed.positionals?.[1] || continuationState.returnHandoffPath || '';
|
|
27
27
|
if (!flags['workspace-id'] && continuationState.workspaceId) flags['workspace-id'] = continuationState.workspaceId;
|
|
28
28
|
if (!flags['workspace-target'] && continuationState.workspaceTarget) flags['workspace-target'] = continuationState.workspaceTarget;
|
|
29
|
+
if (!flags['package-sibling-index'] && continuationState.returnPackageSiblingIndex) flags['package-sibling-index'] = continuationState.returnPackageSiblingIndex;
|
|
29
30
|
if (!flags['package-parent'] && continuationState.packageParentPath) flags['package-parent'] = continuationState.packageParentPath;
|
|
30
31
|
if (!flags.route && handoffPath) flags.route = handoffPath;
|
|
31
32
|
if (!flags.output && !flags['output-dir'] && parsed.surfaceCommand === 'handoff' && continuationState.returnOutputDir) flags['output-dir'] = continuationState.returnOutputDir;
|
|
@@ -135,7 +136,9 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
135
136
|
packageParentWorkspaceIds,
|
|
136
137
|
packageParentWorkspaceAliases,
|
|
137
138
|
reconciliationProof,
|
|
138
|
-
requireReconciliationProof
|
|
139
|
+
requireReconciliationProof,
|
|
140
|
+
returnPackageSiblingIndex: flags['return-package-sibling-index'],
|
|
141
|
+
returnPackageMajor: Boolean(flags['return-package-major'])
|
|
139
142
|
}, runtime);
|
|
140
143
|
return {
|
|
141
144
|
input,
|
|
@@ -83,7 +83,7 @@ function commonCommandHelp(command, surfaceCommand) {
|
|
|
83
83
|
'',
|
|
84
84
|
`${command} handoff <workspace-dir>`,
|
|
85
85
|
'',
|
|
86
|
-
'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>`. Recovery/integration manufacture can bind the reconciled byte proof with `--require-reconciliation-proof --reconciliation-proof <full-reconcile-receipt.json>`; missing, non-ready, edited, or source-stale proof blocks package output. 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.',
|
|
86
|
+
'Infers the latest qualified authored Handoff, selected Workspace identity/target, received package parent as carrier-lineage evidence, canonical projected filename, and return output directory. When the outgoing Handoff declares `Signal Kind: return`, delegation manufacture is transport-not-ready unless the delegator supplies `--return-package-sibling-index <1..9999>` for the expected non-Major return, or explicitly declares `--return-package-major`; the qualified reservation is carried in the route pointer and later continuation state. 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>`. Recovery/integration manufacture can bind the reconciled byte proof with `--require-reconciliation-proof --reconciliation-proof <full-reconcile-receipt.json>`; missing, non-ready, edited, or source-stale proof blocks package output. 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.',
|
|
87
87
|
'',
|
|
88
88
|
`Advanced/internal catalog: ${command} operations`
|
|
89
89
|
];
|
|
@@ -1,13 +1,35 @@
|
|
|
1
|
+
import { prepareNodeSourceFrontier } from '../node/sourceFrontierComparison.js';
|
|
2
|
+
|
|
1
3
|
export async function land(flags, material, readOptionalJson, splitFlag) {
|
|
2
4
|
const repositoriesValue = await readOptionalJson(flags.repositories || flags.repos);
|
|
3
5
|
const selectionsValue = await readOptionalJson(flags.selections);
|
|
6
|
+
const repositories = repositoriesValue.repositories || (Array.isArray(repositoriesValue) ? repositoriesValue : []);
|
|
4
7
|
return {
|
|
5
8
|
input: {
|
|
6
9
|
...material,
|
|
7
|
-
repositories:
|
|
10
|
+
repositories: await attachExactTargetSnapshots(repositories, flags),
|
|
8
11
|
selections: selectionsValue.selections || selectionsValue || repositoriesValue.selections || {},
|
|
9
12
|
workspaceIds: splitFlag(flags.workspaces || flags['workspace-ids'])
|
|
10
13
|
},
|
|
11
14
|
options: {}
|
|
12
15
|
};
|
|
13
16
|
}
|
|
17
|
+
|
|
18
|
+
async function attachExactTargetSnapshots(repositories = [], flags = {}) {
|
|
19
|
+
return Promise.all(repositories.map(async (repository, index) => {
|
|
20
|
+
if (repository?.sourceSnapshot || repository?.snapshot || !String(repository?.root || repository?.path || '').trim()) return repository;
|
|
21
|
+
const root = String(repository.root || repository.path || '').trim();
|
|
22
|
+
const workspaceId = String(repository.workspaceId || repository.id || `landing-target-${index + 1}`);
|
|
23
|
+
const frontier = await prepareNodeSourceFrontier({ kind: 'local-workspace', path: root, workspaceId, label: `workspace-landing-target:${workspaceId}` }, { maxFiles: flags['max-files'] });
|
|
24
|
+
const workspace = (frontier.workspaces || [])[0] || null;
|
|
25
|
+
return {
|
|
26
|
+
...repository,
|
|
27
|
+
sourceSnapshot: workspace?.snapshot || {
|
|
28
|
+
state: 'qualification-error',
|
|
29
|
+
entries: [],
|
|
30
|
+
evidence: {},
|
|
31
|
+
findings: frontier.findings || []
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
@@ -425,7 +425,7 @@ function withCliPhaseTiming(result = {}, timing = {}) {
|
|
|
425
425
|
function parseArgs(argv=[]) {
|
|
426
426
|
const args=[...argv],first=args.shift()||'';
|
|
427
427
|
if(first==='--help'||first==='-h') return {command:'help',flags:{help:true},positionals:[]};
|
|
428
|
-
const command=({orient:'orient-handoff-package',ground:'project-grounding-readiness',receive:'qualify-cold-start',validate:'audit-handoff-package-context',handoff:'manufacture-handoff-package',author:'author',compare:'compare-source-frontiers',reconcile:'prove-source-reconciliation'})[first]||first;
|
|
428
|
+
const command=({orient:'orient-handoff-package',ground:'project-grounding-readiness',receive:'qualify-cold-start',validate:'audit-handoff-package-context',handoff:'manufacture-handoff-package',author:'author',compare:'compare-source-frontiers',reconcile:'prove-source-reconciliation','audit-recovery':'audit-recovery-acceptance'})[first]||first;
|
|
429
429
|
const flags={},positionals=[];
|
|
430
430
|
while(args.length){const token=args.shift();if(!token.startsWith('--')){positionals.push(token);continue;}const key=token.slice(2);flags[key]=!args.length||args[0].startsWith('--')?true:args.shift();}
|
|
431
431
|
return {command,flags,positionals,surfaceCommand:first};
|
|
@@ -9,6 +9,7 @@ import { enumerateNodeWorkspace, PORTABLE_NODE_WORKSPACE_ENUMERATION_SCHEMA_ID }
|
|
|
9
9
|
import { preparePackageParentWorkspaceReuse } from './handoff.manufacture.packageParent.js';
|
|
10
10
|
import { qualifyPortableSourceReconciliationProofForManufacture } from '../../comparison/sourceFrontierReconciliationProof.js';
|
|
11
11
|
import { qualifyPortableManufactureSchemaReferenceCandidate } from '../../handoff/schemaReferencePreflight.js';
|
|
12
|
+
import { qualifyDelegationReturnReservation } from '../../handoff/delegationReturnReservation.js';
|
|
12
13
|
import {
|
|
13
14
|
assertInside,
|
|
14
15
|
expandPointerDependencyClosure,
|
|
@@ -102,6 +103,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
const schemaReferencePreflight = qualifyPortableManufactureSchemaReferenceCandidate(handoff);
|
|
106
|
+
const returnCarrierReservationPreflight = qualifyDelegationReturnReservation({ markdown: handoffMarkdown, returnPackageSiblingIndex: input.returnPackageSiblingIndex, returnPackageMajor: input.returnPackageMajor === true });
|
|
105
107
|
|
|
106
108
|
if (enumeration.status !== 'qualified-complete') throw new Error(`portable.handoff-manufacture.workspace-enumeration.${enumeration.status}`);
|
|
107
109
|
const workspaceTitle = requestedWorkspaceTitle || inferWorkspaceTitle(enumeration) || workspaceId;
|
|
@@ -128,7 +130,11 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
128
130
|
if (!id || workspaceRuntimeById.has(id)) continue;
|
|
129
131
|
workspaceRuntimeById.set(id, Object.freeze({ id, root: '', enumeration: provided.enumeration, provider: 'qualified-package-parent-workspace-material-provider' }));
|
|
130
132
|
}
|
|
131
|
-
const
|
|
133
|
+
const suppliedTransportRoutes = [...(input.transportRoutes || input.handoffRoutes || [])].map((route) => normalizeTransportRoute(route, workspaceId)).filter(Boolean);
|
|
134
|
+
const reservationProjection = returnCarrierReservationPreflight.state === 'qualified' && returnCarrierReservationPreflight.returnExpected
|
|
135
|
+
? Object.freeze({ carrierKind: returnCarrierReservationPreflight.carrierKind, siblingIndex: returnCarrierReservationPreflight.siblingIndex })
|
|
136
|
+
: null;
|
|
137
|
+
const transportRoutes = Object.freeze((suppliedTransportRoutes.length ? suppliedTransportRoutes : [Object.freeze({ workspaceId, path: handoffPath })]).map((route) => Object.freeze({ ...route, ...(route.path === handoffPath && String(route.workspaceId || '') === workspaceId && reservationProjection ? { returnCarrierReservation: reservationProjection } : {}) })));
|
|
132
138
|
const workspaceTargets = mergeWorkspaceTargetBindings(normalizeWorkspaceTargetBindings({
|
|
133
139
|
primaryWorkspaceId: workspaceId,
|
|
134
140
|
primaryTargetPath: input.workspaceTargetPath || input.workspaceArtifactPath || '',
|
|
@@ -195,6 +201,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
195
201
|
toolingBootstrap: toolingBootstrap.summary,
|
|
196
202
|
reconciliationProofQualification,
|
|
197
203
|
schemaReferencePreflight,
|
|
204
|
+
returnCarrierReservationPreflight,
|
|
198
205
|
manufacturingEvidence: Object.freeze({
|
|
199
206
|
enumeration: enumeration.evidence,
|
|
200
207
|
workspaceEnumerations: Object.freeze(workspaceEnumerations),
|
|
@@ -202,6 +209,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
202
209
|
runtimeSourceAlignment,
|
|
203
210
|
reconciliationProof: reconciliationProofQualification,
|
|
204
211
|
schemaReferencePreflight,
|
|
212
|
+
returnCarrierReservationPreflight,
|
|
205
213
|
packageParentWorkspaceReuse: Object.freeze({
|
|
206
214
|
state: String(packageParentReuse.state || ''),
|
|
207
215
|
providerState: String(packageParentReuse.providerState || ''),
|
|
@@ -58,7 +58,7 @@ export function inspectHandoffCarrierProjection(bundle = {}, options = {}) {
|
|
|
58
58
|
if (projection && projection.schema !== HANDOFF_CARRIER_PROJECTION_SCHEMA_ID) findings.push(finding('error', 'portable.handoff-carrier.schema.invalid', 'Handoff carrier projection schema/version is unsupported.'));
|
|
59
59
|
if (projection && projection.boundary !== BOUNDARY) findings.push(finding('error', 'portable.handoff-carrier.boundary.invalid', 'Handoff carrier projection lost its disposable non-authoritative boundary.'));
|
|
60
60
|
if (projection) {
|
|
61
|
-
const expected = buildHandoffCarrierProjection({ bundle, workspaceByteProvider: options.workspaceByteProvider || null, carrierLineage: projection.lineage || null, routes: (projection.routes || []).map((route) => ({ workspaceId: route.workspaceId, path: route.workspaceRelativePath, purpose: route.purpose, participantRoles: route.participantRoleSpecs || [] })) });
|
|
61
|
+
const expected = buildHandoffCarrierProjection({ bundle, workspaceByteProvider: options.workspaceByteProvider || null, carrierLineage: projection.lineage || null, routes: (projection.routes || []).map((route) => ({ workspaceId: route.workspaceId, path: route.workspaceRelativePath, purpose: route.purpose, participantRoles: route.participantRoleSpecs || [], returnCarrierReservation: route.returnCarrierReservation || null })) });
|
|
62
62
|
for (const field of ['status', 'mode', 'lineage', 'workspaces', 'workspace', 'selection', 'routes', 'authority']) {
|
|
63
63
|
if (stableJson(expected[field]) !== stableJson(projection[field])) findings.push(finding('error', `portable.handoff-carrier.${field}.mismatch`, `Handoff carrier ${field} diverges from current package/workspace truth.`));
|
|
64
64
|
}
|
|
@@ -162,7 +162,7 @@ function normalizeRouteSpecs(value, descriptor, defaultWorkspace = null) {
|
|
|
162
162
|
const path = normalizeWorkspacePath(spec.path || spec.workspaceRelativePath || '');
|
|
163
163
|
const workspaceId = String(spec.workspaceId || spec.workspace || defaultWorkspaceId || '');
|
|
164
164
|
const key = `${workspaceId}\u0000${path}`;
|
|
165
|
-
if (path && !map.has(key)) map.set(key, Object.freeze({ workspaceId, path, purpose: String(spec.purpose || ''), participantRoles: Object.freeze([...(spec.participantRoles || spec.roles || [])].map((entry) => typeof entry === 'string' ? entry : Object.freeze({ ...(entry || {}) }))) }));
|
|
165
|
+
if (path && !map.has(key)) map.set(key, Object.freeze({ workspaceId, path, purpose: String(spec.purpose || ''), participantRoles: Object.freeze([...(spec.participantRoles || spec.roles || [])].map((entry) => typeof entry === 'string' ? entry : Object.freeze({ ...(entry || {}) }))), returnCarrierReservation: spec.returnCarrierReservation ? Object.freeze({ ...(spec.returnCarrierReservation || {}) }) : null }));
|
|
166
166
|
}
|
|
167
167
|
return [...map.values()].sort((a, b) => a.workspaceId.localeCompare(b.workspaceId) || a.path.localeCompare(b.path));
|
|
168
168
|
}
|
|
@@ -53,6 +53,7 @@ export function qualifyRoute(bundle, descriptor, byteProvider, workspace, spec =
|
|
|
53
53
|
materialRequirements,
|
|
54
54
|
participantRoles,
|
|
55
55
|
participantRoleSpecs,
|
|
56
|
+
returnCarrierReservation: spec.returnCarrierReservation ? deepFreeze({ ...(spec.returnCarrierReservation || {}) }) : null,
|
|
56
57
|
requiredClosure,
|
|
57
58
|
reasons: Object.freeze(reasons),
|
|
58
59
|
authority: Object.freeze({ artifactPartiesAuthoritative: true, dimensionSemanticAuthority: false, filenameSemanticAuthority: false })
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const PORTABLE_DELEGATION_RETURN_RESERVATION_SCHEMA_ID = 'tiinex.portable.delegation-return-reservation-preflight.v1';
|
|
2
|
+
|
|
3
|
+
export function qualifyDelegationReturnReservation(input = {}) {
|
|
4
|
+
const markdown = String(input.markdown || input.handoffMarkdown || '');
|
|
5
|
+
const completion = sectionText(markdown, 'Completion Expectation');
|
|
6
|
+
const signalKind = field(completion, 'Signal Kind').toLowerCase();
|
|
7
|
+
const returnExpected = signalKind === 'return';
|
|
8
|
+
const declaredMajor = input.returnPackageMajor === true;
|
|
9
|
+
const rawIndex = firstDefined(input.returnPackageSiblingIndex, reservedSiblingIndexFromHandoff(markdown));
|
|
10
|
+
const findings = [];
|
|
11
|
+
|
|
12
|
+
if (!returnExpected) return freeze({
|
|
13
|
+
schema: PORTABLE_DELEGATION_RETURN_RESERVATION_SCHEMA_ID,
|
|
14
|
+
state: 'not-required',
|
|
15
|
+
returnExpected: false,
|
|
16
|
+
carrierKind: 'not-applicable',
|
|
17
|
+
siblingIndex: null,
|
|
18
|
+
findings,
|
|
19
|
+
boundary: boundary()
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (declaredMajor) {
|
|
23
|
+
if (rawIndex !== undefined && rawIndex !== null && String(rawIndex).trim() !== '') findings.push(finding('error', 'portable.delegation-return-reservation.major-index-conflict', 'A Major return declaration must not also carry a non-Major package sibling index.'));
|
|
24
|
+
return freeze({
|
|
25
|
+
schema: PORTABLE_DELEGATION_RETURN_RESERVATION_SCHEMA_ID,
|
|
26
|
+
state: findings.length ? 'blocked' : 'qualified',
|
|
27
|
+
returnExpected: true,
|
|
28
|
+
carrierKind: 'major',
|
|
29
|
+
siblingIndex: null,
|
|
30
|
+
findings,
|
|
31
|
+
boundary: boundary()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const siblingIndex = parseSiblingIndex(rawIndex);
|
|
36
|
+
if (siblingIndex === null) findings.push(finding('error', 'portable.delegation-return-reservation.sibling-index.required', 'A return Handoff is transport-not-ready until the delegator supplies one explicit non-Major return package sibling index in the supported range 1..9999.'));
|
|
37
|
+
return freeze({
|
|
38
|
+
schema: PORTABLE_DELEGATION_RETURN_RESERVATION_SCHEMA_ID,
|
|
39
|
+
state: findings.length ? 'blocked' : 'qualified',
|
|
40
|
+
returnExpected: true,
|
|
41
|
+
carrierKind: 'non-major',
|
|
42
|
+
siblingIndex,
|
|
43
|
+
findings,
|
|
44
|
+
boundary: boundary()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseDelegationReturnReservationPointer(pointer = {}) {
|
|
49
|
+
const siblingIndex = parseSiblingIndex(pointer.returnPackageSiblingIndex);
|
|
50
|
+
const carrierKind = String(pointer.returnPackageCarrierKind || '').trim().toLowerCase();
|
|
51
|
+
if (carrierKind === 'major') return freeze({ state: 'qualified', returnExpected: true, carrierKind: 'major', siblingIndex: null });
|
|
52
|
+
if (carrierKind === 'non-major' && siblingIndex !== null) return freeze({ state: 'qualified', returnExpected: true, carrierKind: 'non-major', siblingIndex });
|
|
53
|
+
return freeze({ state: 'unresolved', returnExpected: false, carrierKind: '', siblingIndex: null });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function reservedSiblingIndexFromHandoff(markdown = '') {
|
|
58
|
+
const transfers = sectionText(markdown, 'Transfers');
|
|
59
|
+
if (!transfers) return null;
|
|
60
|
+
const lines = transfers.replace(/\r\n?/g, '\n').split('\n');
|
|
61
|
+
const start = lines.findIndex((line) => /^\s*-\s+reserved-return-package-sibling-index\s*$/i.test(line));
|
|
62
|
+
if (start < 0) return null;
|
|
63
|
+
const block = [];
|
|
64
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
65
|
+
if (/^-\s+[^\s].*$/.test(lines[index])) break;
|
|
66
|
+
block.push(lines[index]);
|
|
67
|
+
}
|
|
68
|
+
const description = field(block.join('\n'), 'Description');
|
|
69
|
+
const match = description.match(/(?:package\s+sibling\s+index|sibling\s+index|index)\s+`?(\d{1,4})`?/i);
|
|
70
|
+
return match ? match[1] : null;
|
|
71
|
+
}
|
|
72
|
+
function firstDefined(...values) {
|
|
73
|
+
for (const value of values) if (value !== undefined && value !== null && String(value).trim() !== '') return value;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseSiblingIndex(value) {
|
|
78
|
+
if (value === undefined || value === null || String(value).trim() === '') return null;
|
|
79
|
+
if (!/^\d+$/.test(String(value).trim())) return null;
|
|
80
|
+
const parsed = Number.parseInt(String(value).trim(), 10);
|
|
81
|
+
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 9999 ? parsed : null;
|
|
82
|
+
}
|
|
83
|
+
function sectionText(markdown = '', heading = '') {
|
|
84
|
+
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
85
|
+
const start = lines.findIndex((line) => new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'i').test(line));
|
|
86
|
+
if (start < 0) return '';
|
|
87
|
+
const out = [];
|
|
88
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
89
|
+
if (/^##\s+/.test(lines[index])) break;
|
|
90
|
+
out.push(lines[index]);
|
|
91
|
+
}
|
|
92
|
+
return out.join('\n');
|
|
93
|
+
}
|
|
94
|
+
function field(section = '', name = '') {
|
|
95
|
+
const match = String(section || '').match(new RegExp(`^\\s*-\\s+${escapeRegExp(name)}\\s*:\\s*(.+?)\\s*$`, 'mi'));
|
|
96
|
+
return String(match?.[1] || '').trim();
|
|
97
|
+
}
|
|
98
|
+
function boundary() { return 'Transport preflight only. It validates an explicit delegator-coordinated return reservation and never allocates, discovers, increments, guesses, recycles, or promotes a sibling index into semantic Parent, Workspace, Role, acceptance, or completion authority.'; }
|
|
99
|
+
function finding(severity, code, message) { return Object.freeze({ severity, code, message }); }
|
|
100
|
+
function escapeRegExp(value = '') { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
101
|
+
function freeze(value) { if (Array.isArray(value)) return Object.freeze(value.map(freeze)); if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, freeze(item)]))); }
|
|
@@ -21,9 +21,12 @@ export function manufactureRecipientRelativeHandoffPackage(input = {}, options =
|
|
|
21
21
|
const reconciliationBlocked = String(reconciliationProofQualification?.state || '') === 'blocked';
|
|
22
22
|
const schemaReferencePreflight = input.schemaReferencePreflight || input.manufacturingEvidence?.schemaReferencePreflight || null;
|
|
23
23
|
const schemaReferenceBlocked = String(schemaReferencePreflight?.state || '') === 'blocked';
|
|
24
|
+
const returnCarrierReservationPreflight = input.returnCarrierReservationPreflight || input.manufacturingEvidence?.returnCarrierReservationPreflight || null;
|
|
25
|
+
const returnCarrierReservationBlocked = String(returnCarrierReservationPreflight?.state || '') === 'blocked';
|
|
24
26
|
const findings = Object.freeze([
|
|
25
27
|
...majorFindings,
|
|
26
28
|
...(schemaReferencePreflight?.findings || []),
|
|
29
|
+
...(returnCarrierReservationPreflight?.findings || []),
|
|
27
30
|
...(reconciliationProofQualification?.findings || []),
|
|
28
31
|
...(baseline.findings || []),
|
|
29
32
|
...(upgraded.findings || []),
|
|
@@ -36,7 +39,7 @@ export function manufactureRecipientRelativeHandoffPackage(input = {}, options =
|
|
|
36
39
|
...(upgraded.roundtrip?.findings || []),
|
|
37
40
|
...(toolingBootstrapInspection?.findings || [])
|
|
38
41
|
]);
|
|
39
|
-
const status = baseline.status !== 'blocked' && upgraded.status !== 'blocked' && toolingBootstrapInspection?.status === 'valid' && majorReadiness.state !== 'blocked' && !schemaReferenceBlocked && !reconciliationBlocked ? upgraded.status : 'blocked';
|
|
42
|
+
const status = baseline.status !== 'blocked' && upgraded.status !== 'blocked' && toolingBootstrapInspection?.status === 'valid' && majorReadiness.state !== 'blocked' && !schemaReferenceBlocked && !returnCarrierReservationBlocked && !reconciliationBlocked ? upgraded.status : 'blocked';
|
|
40
43
|
return Object.freeze({
|
|
41
44
|
schema: 'tiinex.portable.handoff-manufacturing.v2',
|
|
42
45
|
status,
|
|
@@ -55,6 +58,7 @@ export function manufactureRecipientRelativeHandoffPackage(input = {}, options =
|
|
|
55
58
|
roundtrip: upgraded.roundtrip ? String(upgraded.roundtrip.status || 'unknown') : 'not-requested',
|
|
56
59
|
toolingBootstrap: String(toolingBootstrapInspection?.status || 'unavailable'),
|
|
57
60
|
schemaReferencePreflight: String(schemaReferencePreflight?.state || 'not-run'),
|
|
61
|
+
returnCarrierReservationPreflight: String(returnCarrierReservationPreflight?.state || 'not-run'),
|
|
58
62
|
reconciliationProof: String(reconciliationProofQualification?.state || 'not-required')
|
|
59
63
|
}),
|
|
60
64
|
plan: baseline.plan,
|
|
@@ -74,6 +78,7 @@ export function manufactureRecipientRelativeHandoffPackage(input = {}, options =
|
|
|
74
78
|
toolingBootstrap: input.toolingBootstrap || null,
|
|
75
79
|
manufacturingEvidence: input.manufacturingEvidence || null,
|
|
76
80
|
schemaReferencePreflight,
|
|
81
|
+
returnCarrierReservationPreflight,
|
|
77
82
|
reconciliationProofQualification,
|
|
78
83
|
toolingBootstrapInspection,
|
|
79
84
|
carrierLineage: upgraded.carrierProjection?.lineage || baseline.carrierProjection?.lineage || input.carrierLineage || null,
|
|
@@ -86,6 +91,7 @@ export function manufactureRecipientRelativeHandoffPackage(input = {}, options =
|
|
|
86
91
|
remoteMutation: false,
|
|
87
92
|
physicalRoundtripVerification: upgraded.roundtrip ? String(upgraded.roundtrip.status || 'unknown') : 'not-requested',
|
|
88
93
|
schemaReferencePreflight: String(schemaReferencePreflight?.state || 'not-run'),
|
|
94
|
+
returnCarrierReservationPreflight: String(returnCarrierReservationPreflight?.state || 'not-run'),
|
|
89
95
|
reconciliationProof: String(reconciliationProofQualification?.state || 'not-required'),
|
|
90
96
|
hostBehaviorAuthority: 'none'
|
|
91
97
|
}),
|
|
@@ -115,7 +115,7 @@ export function upgradeRecipientRelativeHandoffTransportPackageV2(baseline = {},
|
|
|
115
115
|
const descriptorFile = finalizeFile({ path: HANDOFF_CLOSURE_DESCRIPTOR_PATH, kind: 'handoff-closure-descriptor', logicalKind: 'disposable-transport-control', mediaType: 'application/json', content: `${stablePrettyJson(descriptor)}\n`, boundary: descriptor.boundary });
|
|
116
116
|
const projectionBundle = { ...baselineBundle, files: [...retained, ...workspaceFiles, descriptorFile], handoffClosure: descriptor };
|
|
117
117
|
const provider = buildDirectArchiveProjectionProvider(records);
|
|
118
|
-
const carrierProjection = buildHandoffCarrierProjection({ bundle: projectionBundle, descriptor, workspaceByteProvider: provider, carrierLineage: input.carrierLineage || baseline.carrierProjection?.lineage || null, routes: input.transportRoutes || input.handoffRoutes || (baseline.carrierProjection?.routes || []).map((route) => ({ workspaceId: route.workspaceId, path: route.workspaceRelativePath, purpose: route.purpose })) });
|
|
118
|
+
const carrierProjection = buildHandoffCarrierProjection({ bundle: projectionBundle, descriptor, workspaceByteProvider: provider, carrierLineage: input.carrierLineage || baseline.carrierProjection?.lineage || null, routes: input.transportRoutes || input.handoffRoutes || (baseline.carrierProjection?.routes || []).map((route) => ({ workspaceId: route.workspaceId, path: route.workspaceRelativePath, purpose: route.purpose, returnCarrierReservation: route.returnCarrierReservation || null })) });
|
|
119
119
|
const createdAt=baselineBundle.manifest?.createdAt||baselineBundle.builtAt||'';
|
|
120
120
|
const transportStatus = baseline.status === 'blocked' || carrierProjection.status !== 'ready' ? 'blocked' : baseline.status;
|
|
121
121
|
const transportCompanion = buildHandoffTransportCompanionProjection({ bundle: projectionBundle, descriptor, packageStatus: transportStatus, participation: input.transportParticipation || input.participation || {} });
|
|
@@ -24,6 +24,17 @@ export function correlatePointerFacts(markdown, facts, findings, path) {
|
|
|
24
24
|
if (facts.role !== 'handoff-route' || !facts.archivePath) return;
|
|
25
25
|
const targets = [...sectionText(markdown, 'Destinations').matchAll(/\[[^\]]*\]\(([^)]+)\)/g)].map((match) => match[1]);
|
|
26
26
|
if (targets.length !== 1 || targets[0] !== String(facts.archivePath)) findings.push(finding('error', 'portable.handoff-v2-surface.pointer.visible-destination-mismatch', 'Route Pointer visible Destination diverges from its sealed machine facts.', { path }));
|
|
27
|
+
if (facts.returnCarrierReservation) {
|
|
28
|
+
const current = sectionText(markdown, 'Current Read');
|
|
29
|
+
const visibleKind = unquoteCode(fieldValue(current, 'Return Package Carrier Kind'));
|
|
30
|
+
const expectedKind = String(facts.returnCarrierReservation.carrierKind || '');
|
|
31
|
+
if (visibleKind !== expectedKind) findings.push(finding('error', 'portable.handoff-v2-surface.pointer.visible-return-carrier-kind-mismatch', 'Route Pointer visible return-carrier kind diverges from its sealed machine facts.', { path }));
|
|
32
|
+
if (expectedKind === 'non-major') {
|
|
33
|
+
const visibleSiblingIndex = Number(unquoteCode(fieldValue(current, 'Return Package Sibling Index')) || 0);
|
|
34
|
+
const expectedSiblingIndex = Number(facts.returnCarrierReservation.siblingIndex || 0);
|
|
35
|
+
if (visibleSiblingIndex !== expectedSiblingIndex) findings.push(finding('error', 'portable.handoff-v2-surface.pointer.visible-return-sibling-index-mismatch', 'Route Pointer visible return package sibling index diverges from its sealed machine facts.', { path }));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
export function markdownTarget(value = '') { return String(value || '').match(/\[[^\]]*\]\(([^)]+)\)/)?.[1] || String(value || '').trim(); }
|
|
29
40
|
export function inspectExternalPayloadShape(markdown, findings, path) {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { parentTrace } from './recipientV2.lineage.js';
|
|
2
2
|
|
|
3
|
-
export function projectRecipientV2Routes(routePointers = [], endpointPointers = [], participantPointers = []) {
|
|
4
|
-
return Object.freeze(routePointers.map((item) =>
|
|
3
|
+
export function projectRecipientV2Routes(routePointers = [], endpointPointers = [], participantPointers = [], qualifiedRoutes = []) {
|
|
4
|
+
return Object.freeze(routePointers.map((item) => {
|
|
5
|
+
const qualified = (qualifiedRoutes || []).find((route) => String(route.workspaceId || '') === String(item.facts?.workspaceId || '') && String(route.workspaceRelativePath || '') === String(item.facts?.workspaceRelativeHandoffPath || '')) || null;
|
|
6
|
+
return Object.freeze({
|
|
5
7
|
pointerPath: item.path,
|
|
6
8
|
workspaceId: String(item.facts?.workspaceId || ''),
|
|
7
9
|
workspaceRelativeHandoffPath: String(item.facts?.workspaceRelativeHandoffPath || ''),
|
|
10
|
+
returnCarrierReservation: item.facts?.returnCarrierReservation || qualified?.returnCarrierReservation || null,
|
|
8
11
|
endpointRolePointers: Object.freeze(rolePointerAncestors(item, endpointPointers, participantPointers).filter((path) => endpointPointers.some((pointer) => pointer.path === path))),
|
|
9
12
|
participantRolePointers: Object.freeze(rolePointerAncestors(item, endpointPointers, participantPointers).filter((path) => participantPointers.some((pointer) => pointer.path === path)))
|
|
10
|
-
})
|
|
13
|
+
});
|
|
14
|
+
}));
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export function projectRecipientV2EndpointRoles(pointers = []) {
|
|
@@ -173,11 +173,12 @@ function buildRecipientFacingV2PackageV1Prepared(input = {}, sealedByWorkspaceId
|
|
|
173
173
|
workspaceId: owningWorkspace.workspaceId, workspaceArtifactPath: owningWorkspace.workspacePath, workspaceArtifactSha256: owningWorkspace.workspaceSha256,
|
|
174
174
|
archivePath: owningWorkspace.archivePath, archiveSha256: owningWorkspace.archiveSha256, sourceWorkspaceTargetInnerPath: owningWorkspace.sourceWorkspaceTargetInnerPath,
|
|
175
175
|
sourceWorkspaceTargetSha256: owningWorkspace.sourceWorkspaceTargetSha256, workspaceRelativeHandoffPath: String(route.workspaceRelativePath || ''), handoffBytes: Number(routeEntry?.bytes || 0), handoffSha256: String(route.sha256 || ''), routeId: String(route.id || ''), parties: route.parties || {}, cacheArtifactPath: cache?.artifactPath || '',
|
|
176
|
+
returnCarrierReservation: route.returnCarrierReservation || null,
|
|
176
177
|
requiredContextBindings: Object.freeze((route.requiredClosure?.requirements || []).filter((entry) => entry.state === 'qualified' && entry.resolution?.kind === 'workspace-archive-entry').map((entry) => Object.freeze({ requirementId: String(entry.requirementId || ''), name: String(entry.name || ''), referenceTarget: String(entry.referenceTarget || ''), workspaceId: String(entry.resolution?.workspaceId || ''), workspaceRelativePath: String(entry.resolution?.workspaceRelativePath || entry.resolution?.innerPath || ''), bytes: Number(entry.resolution?.bytes || 0), sha256: String(entry.resolution?.sha256 || '') })))
|
|
177
178
|
};
|
|
178
|
-
const pointer = finalizeFile({ path: pointerPath, kind: 'handoff-route-pointer', logicalKind: 'recipient-v2-package-v1-handoff-route-pointer', mediaType: 'text/markdown', transportFacts: recipientV2TransportFacts('handoff-route', pointerFacts), content: renderRecipientV2Pointer({ createdAt, parent: lineageParent, role: 'handoff-route', title: `Handoff Route Pointer — ${String(route.parties?.to || owningWorkspace.workspaceId || 'recipient')}`, summary: 'Qualified package-local Pointer to one authoritative Handoff inside one qualified carried Workspace representation.', prose: 'Follow only this Pointer carrier-ancestor closure for pre-Handoff package grounding, then resolve the authoritative Handoff path against the exact qualified carried Workspace representation.', currentRead: [{ label: 'Workspace Id', value: `\`${owningWorkspace.workspaceId}\`` }, { label: 'Workspace', value: `[${owningWorkspace.workspaceId}](${owningWorkspace.workspacePath})` }, { label: 'Route Id', value: `\`${String(route.id || '')}\`` }, ...(pointerFacts.cacheArtifactPath ? [{ label: 'Workspace Dependency Cache', value: `[cache](${pointerFacts.cacheArtifactPath})` }] : []), { label: 'Handoff Workspace Path', value: `\`${String(route.workspaceRelativePath || '')}\`` }], destinations: [{ label: 'Workspace representation containing the qualified Handoff route', display: `${owningWorkspace.archivePath} :: ${String(route.workspaceRelativePath || '')}`, target: owningWorkspace.archivePath }], facts: pointerFacts }) });
|
|
179
|
+
const pointer = finalizeFile({ path: pointerPath, kind: 'handoff-route-pointer', logicalKind: 'recipient-v2-package-v1-handoff-route-pointer', mediaType: 'text/markdown', transportFacts: recipientV2TransportFacts('handoff-route', pointerFacts), content: renderRecipientV2Pointer({ createdAt, parent: lineageParent, role: 'handoff-route', title: `Handoff Route Pointer — ${String(route.parties?.to || owningWorkspace.workspaceId || 'recipient')}`, summary: 'Qualified package-local Pointer to one authoritative Handoff inside one qualified carried Workspace representation.', prose: 'Follow only this Pointer carrier-ancestor closure for pre-Handoff package grounding, then resolve the authoritative Handoff path against the exact qualified carried Workspace representation.', currentRead: [{ label: 'Workspace Id', value: `\`${owningWorkspace.workspaceId}\`` }, { label: 'Workspace', value: `[${owningWorkspace.workspaceId}](${owningWorkspace.workspacePath})` }, { label: 'Route Id', value: `\`${String(route.id || '')}\`` }, ...(pointerFacts.cacheArtifactPath ? [{ label: 'Workspace Dependency Cache', value: `[cache](${pointerFacts.cacheArtifactPath})` }] : []), { label: 'Handoff Workspace Path', value: `\`${String(route.workspaceRelativePath || '')}\`` }, ...(route.returnCarrierReservation ? [{ label: 'Return Package Carrier Kind', value: `\`${String(route.returnCarrierReservation.carrierKind || '')}\`` }, ...(route.returnCarrierReservation.carrierKind === 'non-major' ? [{ label: 'Return Package Sibling Index', value: `\`${String(route.returnCarrierReservation.siblingIndex || '')}\`` }] : [])] : [])], destinations: [{ label: 'Workspace representation containing the qualified Handoff route', display: `${owningWorkspace.archivePath} :: ${String(route.workspaceRelativePath || '')}`, target: owningWorkspace.archivePath }], facts: pointerFacts }) });
|
|
179
180
|
files.push(pointer);
|
|
180
|
-
topology.routes.push(Object.freeze({ pointerPath, workspaceId: owningWorkspace.workspaceId, workspaceRelativeHandoffPath: String(route.workspaceRelativePath || ''), routeId: String(route.id || ''), sha256: String(route.sha256 || '') }));
|
|
181
|
+
topology.routes.push(Object.freeze({ pointerPath, workspaceId: owningWorkspace.workspaceId, workspaceRelativeHandoffPath: String(route.workspaceRelativePath || ''), routeId: String(route.id || ''), sha256: String(route.sha256 || ''), returnCarrierReservation: route.returnCarrierReservation || null }));
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
const readFacts = { format: RECIPIENT_V2_PACKAGE_V1_FORMAT_ID, packageRootPath: RECIPIENT_V2_PACKAGE_V1_ROOT_PATH, entryArtifactPath: RECIPIENT_V2_READ_PATH, artifactSurface: 'tiinex.handoff.package.v1-plus-qualified-carried-material', routeAuthority: 'qualified-handoff-route-pointer-plus-exact-qualified-carried-handoff-bytes', routeSelectionAuthority: RECIPIENT_V2_ROUTE_SELECTION_AUTHORITY, siblingRouteInference: RECIPIENT_V2_SIBLING_ROUTE_INFERENCE, carrierLineage: carrier.lineage || null, pathParentProjection: true, pathAuthority: false };
|
|
@@ -71,7 +71,13 @@ export function deriveVisibleFacts({ file = null, markdown = '', schemaId = '',
|
|
|
71
71
|
const closure = direct || generic;
|
|
72
72
|
const handoffEntry = (closure?.parsed?.entries || []).find((entry) => entry.path === visible.handoffWorkspacePath) || null;
|
|
73
73
|
const current = sectionText(markdown, 'Current Read');
|
|
74
|
-
|
|
74
|
+
const returnCarrierReservation = visible.returnPackageCarrierKind
|
|
75
|
+
? Object.freeze({
|
|
76
|
+
carrierKind: String(visible.returnPackageCarrierKind || ''),
|
|
77
|
+
...(String(visible.returnPackageCarrierKind || '') === 'non-major' ? { siblingIndex: Number(visible.returnPackageSiblingIndex || 0) } : {})
|
|
78
|
+
})
|
|
79
|
+
: null;
|
|
80
|
+
return { ...base, workspaceId, workspaceArtifactPath: closure?.workspaceFile?.path || '', workspaceArtifactSha256: closure?.workspaceFile ? sha256Hex(packageFileBytes(closure.workspaceFile)) : '', archivePath: closure?.archiveFile?.path || '', archiveSha256: closure?.archiveFile ? sha256Hex(packageFileBytes(closure.archiveFile)) : '', sourceWorkspaceTargetInnerPath: closure?.workspaceArtifactInnerPath || '', sourceWorkspaceTargetSha256: closure?.sourceWorkspaceTargetSha256 || '', workspaceRelativeHandoffPath: visible.handoffWorkspacePath, handoffBytes: Number(handoffEntry?.bytes || 0), handoffSha256: String(handoffEntry?.sha256 || ''), routeId: visible.routeId || unquote(field(current, 'Route Id')), cacheArtifactPath: markdownTarget(field(current, 'Workspace Dependency Cache')), returnCarrierReservation, requiredContextBindings: Object.freeze([]) };
|
|
75
81
|
}
|
|
76
82
|
if (role === 'endpoint-role' || role === 'participant-role') {
|
|
77
83
|
const targetPayload = visible.targetPayload || '';
|
|
@@ -16,6 +16,7 @@ import { projectRecipientV2EndpointRoles, projectRecipientV2ParticipantRoles, pr
|
|
|
16
16
|
import { RECIPIENT_V2_PACKAGE_V1_FORMAT_ID, RECIPIENT_V2_PACKAGE_V1_ROOT_PATH, RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID } from './recipientV2.packageV1.constants.js';
|
|
17
17
|
import { BOOTSTRAP_PACKAGE_ROLE, HANDOFF_PACKAGE_ROLE, parseHandoffPackageV1, validatePackageFields, WORKSPACE_PACKAGE_ROLE } from './recipientV2.packageV1.contract.js';
|
|
18
18
|
import { deriveVisibleFacts, validateRouteClosure } from './recipientV2.packageV1.inspect.helpers.js';
|
|
19
|
+
import { qualifyDelegationReturnReservation } from './delegationReturnReservation.js';
|
|
19
20
|
import { bootstrapCarrierProjection, workspaceCarrierProjection } from './recipientV2.packageV1.workspaceProjection.js';
|
|
20
21
|
import { inspectRecipientV2WorkspaceSurface } from './recipientV2.inspect.workspaces.js';
|
|
21
22
|
import { byteEqual, currentSchemaId, decodeUtf8, dedupeFindings, deepFreeze, oneFile } from './recipientV2.packageV1.shared.js';
|
|
@@ -195,7 +196,19 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
195
196
|
} else {
|
|
196
197
|
inspectEndpointRolePointers(endpointRolePointers, workspaceParts, caches, findings);
|
|
197
198
|
inspectParticipantRolePointers(participantRolePointers, workspaceParts, caches, findings);
|
|
198
|
-
const routeSpecs = routePointers.map((pointer) =>
|
|
199
|
+
const routeSpecs = routePointers.map((pointer) => {
|
|
200
|
+
const workspaceId = String(pointer.facts?.workspaceId || '');
|
|
201
|
+
const path = String(pointer.facts?.workspaceRelativeHandoffPath || '');
|
|
202
|
+
let returnCarrierReservation = pointer.facts?.returnCarrierReservation || null;
|
|
203
|
+
if (!returnCarrierReservation && workspaceId && path) {
|
|
204
|
+
const entry = resolveHandoffWorkspaceEntry(workspaceByteProvider, workspaceId, path);
|
|
205
|
+
if (entry.state === 'qualified') {
|
|
206
|
+
const preflight = qualifyDelegationReturnReservation({ markdown: decodeUtf8(entry.data) });
|
|
207
|
+
if (preflight.state === 'qualified' && preflight.returnExpected) returnCarrierReservation = Object.freeze({ carrierKind: preflight.carrierKind, siblingIndex: preflight.siblingIndex });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { workspaceId, path, purpose: '', returnCarrierReservation };
|
|
211
|
+
});
|
|
199
212
|
carrierProjection = buildHandoffCarrierProjection({ bundle: semanticBundle, descriptor, workspaceByteProvider, carrierLineage: lineage, routes: routeSpecs });
|
|
200
213
|
if (carrierProjection.status !== 'ready') findings.push(finding('error', 'portable.handoff-package-v1.routes-unqualified', 'Selected Handoff Pointer does not independently resolve to qualified authoritative Handoff bytes.', { causes: carrierProjection.findings || [] }));
|
|
201
214
|
inspectRoutePointers(routePointers, carrierProjection, workspaceParts, endpointRolePointers, participantRolePointers, index, findings);
|
|
@@ -244,7 +257,7 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
244
257
|
schema: 'tiinex.portable.recipient-facing-handoff-package-v1.inspection.v1', detected: Boolean(packageFile), status, format: RECIPIENT_V2_PACKAGE_V1_FORMAT_ID,
|
|
245
258
|
rootArtifact: packageFile ? Object.freeze({ path: packageFile.path, schemaId: RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID, sha256: sha256Hex(packageFileBytes(packageFile)), carrierLineage: lineage }) : null,
|
|
246
259
|
readArtifact, workspaces: Object.freeze(workspaceParts.map((item) => Object.freeze({ workspaceId: item.workspaceId, coverage: String(item.representation?.coverage || item.facts?.coverage || 'complete'), bindingState: item.bindingState || String(item.representation?.bindingState || 'verified'), workspaceArtifactPath: item.artifact.path, workspaceArchivePath: item.archiveFile?.path || '', sourceWorkspaceTargetInnerPath: item.facts.sourceWorkspaceTargetInnerPath, sourceWorkspaceTargetSha256: item.facts.sourceWorkspaceTargetSha256 }))), sealedWorkspaces: Object.freeze(sealedWorkspaceBindings),
|
|
247
|
-
routes: projectRecipientV2Routes(routePointers, endpointRolePointers, participantRolePointers), endpointRoles: projectRecipientV2EndpointRoles(endpointRolePointers), participantRoles: projectRecipientV2ParticipantRoles(participantRolePointers),
|
|
260
|
+
routes: projectRecipientV2Routes(routePointers, endpointRolePointers, participantRolePointers, carrierProjection?.routes || []), endpointRoles: projectRecipientV2EndpointRoles(endpointRolePointers), participantRoles: projectRecipientV2ParticipantRoles(participantRolePointers),
|
|
248
261
|
caches: Object.freeze(caches.map((cache) => Object.freeze({ workspaceId: String(cache.facts?.workspaceId || ''), artifactPath: cache.artifact.path, archivePath: cache.file.path, materials: cache.facts.materials || [] }))),
|
|
249
262
|
bootstrapInspection, transportManifest: null, artifactFacts: Object.freeze(generatedArtifacts.map((item) => Object.freeze({ path: item.path, facts: item.facts }))), descriptor, workspaceByteProvider, carrierProjection, coldConsumerProjection,
|
|
250
263
|
packageContract, findings: Object.freeze(finalFindings), findingSummary: Object.freeze({ errors: finalFindings.filter((item) => item.severity === 'error').length, findings: finalFindings.length }),
|
|
@@ -6,6 +6,8 @@ export function parseRecipientV2Pointer(markdown = '') {
|
|
|
6
6
|
workspaceId: unquoteCode(fieldValue(current, 'Workspace Id')),
|
|
7
7
|
workspacePayload: markdownTarget(fieldValue(current, 'Workspace Payload')),
|
|
8
8
|
handoffWorkspacePath: unquoteCode(fieldValue(current, 'Handoff Workspace Path')),
|
|
9
|
+
returnPackageCarrierKind: unquoteCode(fieldValue(current, 'Return Package Carrier Kind')),
|
|
10
|
+
returnPackageSiblingIndex: unquoteCode(fieldValue(current, 'Return Package Sibling Index')),
|
|
9
11
|
routeId: unquoteCode(fieldValue(current, 'Route Id')),
|
|
10
12
|
routeSelection: fieldValue(current, 'Route Selection'),
|
|
11
13
|
selectedRouteId: unquoteCode(fieldValue(current, 'Selected Route Id')),
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { comparePortableSourceFrontiers, createPortableSourceFrontier } from '../comparison/sourceFrontierComparison.js';
|
|
2
|
+
import { inspectRecipientFacingV2Topology } from './recipientV2.inspect.js';
|
|
3
|
+
import { handoffWorkspaceProviderForId, listHandoffWorkspaceEntries } from './workspaceByteProvider.js';
|
|
4
|
+
|
|
5
|
+
export const PORTABLE_RECOVERY_ACCEPTANCE_AUDIT_SCHEMA_ID = 'tiinex.portable.recovery-acceptance-audit.v1';
|
|
6
|
+
|
|
7
|
+
export function auditPortableRecoveryAcceptance(input = {}) {
|
|
8
|
+
const basisInspection = inspectRecipientFacingV2Topology(input.basis?.bundle || input.basis || {});
|
|
9
|
+
const candidateInspection = inspectRecipientFacingV2Topology(input.candidate?.bundle || input.candidate || {});
|
|
10
|
+
const findings = [];
|
|
11
|
+
if (String(basisInspection.status || '') !== 'valid') findings.push(finding('error', 'portable.recovery-acceptance.basis-unqualified', 'Recovery acceptance audit requires one independently qualified accepted-basis Handoff package.'));
|
|
12
|
+
if (String(candidateInspection.status || '') !== 'valid') findings.push(finding('error', 'portable.recovery-acceptance.candidate-unqualified', 'Recovery acceptance audit requires one independently qualified candidate Handoff package.'));
|
|
13
|
+
if (findings.some((item) => item.severity === 'error')) return auditResult('blocked', [], findings, basisInspection, candidateInspection, input);
|
|
14
|
+
|
|
15
|
+
const candidateIds = [...new Set((candidateInspection.workspaces || []).map((item) => normalizeId(item.workspaceId)).filter(Boolean))].sort();
|
|
16
|
+
const requestedIds = [...new Set([...(input.workspaceIds || input.selectedWorkspaceIds || [])].map(normalizeId).filter(Boolean))].sort();
|
|
17
|
+
const ids = requestedIds.length ? requestedIds : candidateIds;
|
|
18
|
+
const expectedRemovals = normalizeExpectedRemovals(input.expectedRemovals || {});
|
|
19
|
+
const workspaces = ids.map((workspaceId) => auditWorkspace({ workspaceId, basisInspection, candidateInspection, expectedRemovals, findings }));
|
|
20
|
+
for (const workspaceId of requestedIds) if (!candidateIds.includes(workspaceId)) findings.push(finding('error', 'portable.recovery-acceptance.workspace-unresolved', 'Explicitly requested candidate Workspace is not present in the qualified candidate carrier.', { workspaceId }));
|
|
21
|
+
const status = findings.some((item) => item.severity === 'error') || workspaces.some((item) => item.state !== 'ready') ? 'blocked' : 'ready';
|
|
22
|
+
return auditResult(status, workspaces, findings, basisInspection, candidateInspection, input);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function auditWorkspace({ workspaceId, basisInspection, candidateInspection, expectedRemovals, findings }) {
|
|
26
|
+
const basisProvider = handoffWorkspaceProviderForId(basisInspection.workspaceByteProvider, workspaceId);
|
|
27
|
+
const candidateProvider = handoffWorkspaceProviderForId(candidateInspection.workspaceByteProvider, workspaceId);
|
|
28
|
+
const candidateTopology = (candidateInspection.workspaces || []).find((item) => normalizeId(item.workspaceId) === workspaceId) || null;
|
|
29
|
+
const basisTopology = (basisInspection.workspaces || []).find((item) => normalizeId(item.workspaceId) === workspaceId) || null;
|
|
30
|
+
const reasons = [];
|
|
31
|
+
if (basisProvider.state !== 'qualified') {
|
|
32
|
+
reasons.push('basis-workspace-unqualified');
|
|
33
|
+
findings.push(finding('error', 'portable.recovery-acceptance.basis-workspace-unqualified', 'Accepted-basis Workspace bytes are unavailable or unqualified.', { workspaceId, state: String(basisProvider.state || '') }));
|
|
34
|
+
}
|
|
35
|
+
if (candidateProvider.state !== 'qualified') {
|
|
36
|
+
reasons.push('candidate-workspace-unqualified');
|
|
37
|
+
findings.push(finding('error', 'portable.recovery-acceptance.candidate-workspace-unqualified', 'Candidate Workspace bytes are unavailable or unqualified.', { workspaceId, state: String(candidateProvider.state || '') }));
|
|
38
|
+
}
|
|
39
|
+
const coverage = String(candidateTopology?.coverage || '');
|
|
40
|
+
if (coverage !== 'complete') {
|
|
41
|
+
reasons.push('candidate-workspace-not-complete');
|
|
42
|
+
findings.push(finding('error', 'portable.recovery-acceptance.candidate-workspace-not-complete', 'Master Recovery acceptance requires a complete candidate Workspace representation; bounded coverage cannot prove restart suitability.', { workspaceId, coverage }));
|
|
43
|
+
}
|
|
44
|
+
if (reasons.length) return freeze({ workspaceId, state: 'blocked', coverage, reasons: freeze(reasons), comparisonState: 'unavailable', counts: emptyCounts(), unexplainedRemovals: freeze([]), expectedRemovals: freeze([...expectedRemovals.get(workspaceId) || []]), materialization: materializationReceipt(candidateProvider, candidateTopology) });
|
|
45
|
+
|
|
46
|
+
const basis = frontierForProvider(workspaceId, basisProvider, 'accepted-basis');
|
|
47
|
+
const candidate = frontierForProvider(workspaceId, candidateProvider, 'candidate-recovery');
|
|
48
|
+
const comparison = comparePortableSourceFrontiers({ left: basis, right: candidate });
|
|
49
|
+
const workspace = (comparison.workspaces || []).find((item) => normalizeId(item.workspaceId) === workspaceId) || null;
|
|
50
|
+
if (comparison.status !== 'ready' || !workspace || !workspace.delta) {
|
|
51
|
+
reasons.push('workspace-comparison-unqualified');
|
|
52
|
+
findings.push(finding('error', 'portable.recovery-acceptance.workspace-comparison-unqualified', 'Accepted-basis to candidate Workspace comparison did not qualify.', { workspaceId, state: String(workspace?.state || comparison.state || '') }));
|
|
53
|
+
return freeze({ workspaceId, state: 'blocked', coverage, reasons: freeze(reasons), comparisonState: String(workspace?.state || comparison.state || ''), counts: emptyCounts(), unexplainedRemovals: freeze([]), expectedRemovals: freeze([...expectedRemovals.get(workspaceId) || []]), materialization: materializationReceipt(candidateProvider, candidateTopology) });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const removals = (workspace.delta.removed || []).map((entry) => String(entry.path || '')).filter(Boolean);
|
|
57
|
+
const expected = expectedRemovals.get(workspaceId) || new Set();
|
|
58
|
+
const unexplained = removals.filter((path) => !expected.has(path));
|
|
59
|
+
const staleExpected = [...expected].filter((path) => !removals.includes(path));
|
|
60
|
+
if (unexplained.length) {
|
|
61
|
+
reasons.push('unexplained-removals');
|
|
62
|
+
findings.push(finding('error', 'portable.recovery-acceptance.unexplained-removals', 'Candidate Recovery removes accepted-basis source paths without an explicit expected-removal disposition.', { workspaceId, count: unexplained.length, paths: unexplained }));
|
|
63
|
+
}
|
|
64
|
+
if (staleExpected.length) findings.push(finding('warning', 'portable.recovery-acceptance.expected-removal-stale', 'One or more declared expected removals are not removals in the candidate carrier.', { workspaceId, count: staleExpected.length, paths: staleExpected }));
|
|
65
|
+
return freeze({
|
|
66
|
+
workspaceId,
|
|
67
|
+
state: reasons.length ? 'blocked' : 'ready',
|
|
68
|
+
coverage,
|
|
69
|
+
reasons: freeze(reasons),
|
|
70
|
+
basisCoverage: String(basisTopology?.coverage || ''),
|
|
71
|
+
comparisonState: String(workspace.state || ''),
|
|
72
|
+
counts: freeze({
|
|
73
|
+
additions: Number(workspace.delta.counts?.added || 0),
|
|
74
|
+
removals: Number(workspace.delta.counts?.removed || 0),
|
|
75
|
+
byteChanged: Number(workspace.delta.counts?.byteChanged || 0),
|
|
76
|
+
unexplainedRemovals: unexplained.length,
|
|
77
|
+
totalChanges: Number(workspace.delta.counts?.total || 0)
|
|
78
|
+
}),
|
|
79
|
+
unexplainedRemovals: freeze(unexplained),
|
|
80
|
+
expectedRemovals: freeze([...expected]),
|
|
81
|
+
staleExpectedRemovals: freeze(staleExpected),
|
|
82
|
+
materialization: materializationReceipt(candidateProvider, candidateTopology)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function auditResult(status, workspaces, findings, basisInspection, candidateInspection, input) {
|
|
87
|
+
const unexplainedRemovalCount = workspaces.reduce((sum, item) => sum + Number(item.counts?.unexplainedRemovals || 0), 0);
|
|
88
|
+
const completeWorkspaceCount = workspaces.filter((item) => item.coverage === 'complete').length;
|
|
89
|
+
const allReady = workspaces.length > 0 && workspaces.every((item) => item.state === 'ready');
|
|
90
|
+
return freeze({
|
|
91
|
+
schema: PORTABLE_RECOVERY_ACCEPTANCE_AUDIT_SCHEMA_ID,
|
|
92
|
+
status,
|
|
93
|
+
state: status === 'ready' && allReady ? 'acceptance-audit-ready' : 'acceptance-audit-blocked',
|
|
94
|
+
basisQualification: String(basisInspection.status || 'invalid'),
|
|
95
|
+
candidateQualification: String(candidateInspection.status || 'invalid'),
|
|
96
|
+
selectionMode: (input.workspaceIds || input.selectedWorkspaceIds || []).length ? 'explicit-workspace-set' : 'all-candidate-workspaces',
|
|
97
|
+
workspaces: freeze(workspaces),
|
|
98
|
+
counts: freeze({ workspaces: workspaces.length, readyWorkspaces: workspaces.filter((item) => item.state === 'ready').length, completeWorkspaces: completeWorkspaceCount, unexplainedRemovals: unexplainedRemovalCount }),
|
|
99
|
+
suitability: freeze({
|
|
100
|
+
state: status === 'ready' && allReady && unexplainedRemovalCount === 0 ? 'restart-source-ready' : 'blocked',
|
|
101
|
+
candidateCarrierQualified: String(candidateInspection.status || '') === 'valid',
|
|
102
|
+
exactWorkspaceMaterializationQualified: workspaces.length > 0 && workspaces.every((item) => item.materialization?.state === 'qualified'),
|
|
103
|
+
allSelectedWorkspacesComplete: workspaces.length > 0 && completeWorkspaceCount === workspaces.length,
|
|
104
|
+
unexplainedRemovalCount,
|
|
105
|
+
gitCommitStateProven: false,
|
|
106
|
+
semanticAcceptanceGranted: false
|
|
107
|
+
}),
|
|
108
|
+
findings: freeze(findings),
|
|
109
|
+
boundary: 'Coarse Recovery acceptance audit over already-qualified carrier bytes. It decodes the exact candidate Workspace representations, compares them to one explicit accepted basis, and fails on unexplained source removals or incomplete coverage. It does not inspect a live checkout, prove Git cleanliness/committability, decide semantic correctness, authorize deletion, or grant Master Recovery acceptance; target landing still requires the separate exact source preflight.'
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function frontierForProvider(workspaceId, provider, label) {
|
|
114
|
+
const entries = listHandoffWorkspaceEntries({ workspaces: [provider] }, workspaceId);
|
|
115
|
+
return createPortableSourceFrontier({ id: `${label}:${workspaceId}`, source: { kind: label, workspaceId }, workspaces: [{ workspaceId, entries: entries.map(entryIdentity) }] });
|
|
116
|
+
}
|
|
117
|
+
function materializationReceipt(provider, topology) {
|
|
118
|
+
return freeze({ state: provider?.state === 'qualified' ? 'qualified' : 'unqualified', mode: String(provider?.mode || ''), coverage: String(topology?.coverage || ''), entryCount: Array.isArray(provider?.entries) ? provider.entries.length : 0, archivePackagePath: String(provider?.archive?.packagePath || '') });
|
|
119
|
+
}
|
|
120
|
+
function normalizeExpectedRemovals(value) {
|
|
121
|
+
const source = value?.expectedRemovals && typeof value.expectedRemovals === 'object' ? value.expectedRemovals : value;
|
|
122
|
+
const out = new Map();
|
|
123
|
+
if (Array.isArray(source)) {
|
|
124
|
+
for (const item of source) {
|
|
125
|
+
const workspaceId = normalizeId(item?.workspaceId || item?.workspace || '');
|
|
126
|
+
const path = normalizePath(item?.path || '');
|
|
127
|
+
if (!workspaceId || !path) continue;
|
|
128
|
+
if (!out.has(workspaceId)) out.set(workspaceId, new Set());
|
|
129
|
+
out.get(workspaceId).add(path);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
for (const [key, paths] of Object.entries(source || {})) {
|
|
134
|
+
const workspaceId = normalizeId(key);
|
|
135
|
+
if (!workspaceId) continue;
|
|
136
|
+
out.set(workspaceId, new Set((Array.isArray(paths) ? paths : [paths]).map(normalizePath).filter(Boolean)));
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function normalizeId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''); }
|
|
141
|
+
function normalizePath(value = '') { return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').trim(); }
|
|
142
|
+
function entryIdentity(entry = {}) { return freeze({ path: String(entry.path || entry.innerPath || ''), bytes: Number(entry.bytes ?? entry.size ?? 0), sha256: String(entry.sha256 || '').toLowerCase() }); }
|
|
143
|
+
function emptyCounts() { return freeze({ additions: 0, removals: 0, byteChanged: 0, unexplainedRemovals: 0, totalChanges: 0 }); }
|
|
144
|
+
function finding(severity, code, message, context = {}) { return freeze({ severity, code, message, context: freeze({ ...context }) }); }
|
|
145
|
+
function freeze(value) { if (Array.isArray(value)) return Object.freeze(value.map((item) => freeze(item))); if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, freeze(item)]))); }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPortableSourceFrontier, reconcilePortableSourceFrontiers } from '../comparison/sourceFrontierComparison.js';
|
|
1
2
|
import { inspectRecipientFacingV2Topology } from './recipientV2.inspect.js';
|
|
2
3
|
import { parseWorkspaceEntrypoints, normalizeRepositoryIdentity } from './workspaceSourceIdentity.js';
|
|
3
4
|
|
|
@@ -42,25 +43,25 @@ function planWorkspace({ workspace = {}, provider = null, repositories = [], sel
|
|
|
42
43
|
};
|
|
43
44
|
if (!provider || provider.state !== 'qualified' || provider.mode !== 'archive') {
|
|
44
45
|
if (explicit) findings.push(finding('error', 'portable.workspace-landing.workspace-provider-unqualified', 'Requested Workspace lacks a qualified archive byte provider.', { workspaceId }));
|
|
45
|
-
return freeze({ ...base, state: 'unavailable', source: null, repository: null, reasons: ['workspace-provider-unqualified'] });
|
|
46
|
+
return freeze({ ...base, state: 'unavailable', source: null, repository: null, preflight: unavailablePreflight('workspace-provider-unqualified'), reasons: ['workspace-provider-unqualified'] });
|
|
46
47
|
}
|
|
47
48
|
const target = (provider.entries || []).find((entry) => String(entry.path || '') === base.workspaceArtifactInnerPath);
|
|
48
49
|
if (!target?.data) {
|
|
49
50
|
if (explicit) findings.push(finding('error', 'portable.workspace-landing.workspace-target-unavailable', 'Requested Workspace durable source artifact bytes are unavailable.', { workspaceId }));
|
|
50
|
-
return freeze({ ...base, state: 'unavailable', source: null, repository: null, reasons: ['workspace-target-unavailable'] });
|
|
51
|
+
return freeze({ ...base, state: 'unavailable', source: null, repository: null, preflight: unavailablePreflight('workspace-target-unavailable'), reasons: ['workspace-target-unavailable'] });
|
|
51
52
|
}
|
|
52
53
|
let markdown = '';
|
|
53
54
|
try { markdown = new TextDecoder('utf-8', { fatal: true }).decode(byteView(target.data)); }
|
|
54
55
|
catch {
|
|
55
56
|
findings.push(finding('error', 'portable.workspace-landing.workspace-target-nontext', 'Qualified Workspace target could not be decoded as UTF-8 Markdown.', { workspaceId }));
|
|
56
|
-
return freeze({ ...base, state: 'unavailable', source: null, repository: null, reasons: ['workspace-target-nontext'] });
|
|
57
|
+
return freeze({ ...base, state: 'unavailable', source: null, repository: null, preflight: unavailablePreflight('workspace-target-nontext'), reasons: ['workspace-target-nontext'] });
|
|
57
58
|
}
|
|
58
59
|
const sources = parseWorkspaceEntrypoints(markdown);
|
|
59
60
|
const repoSources = sources.filter((source) => normalizeRepositoryIdentity(source.repository));
|
|
60
61
|
const sourceIdentities = [...new Set(repoSources.map((source) => normalizeRepositoryIdentity(source.repository)))];
|
|
61
62
|
if (sourceIdentities.length !== 1) {
|
|
62
63
|
if (explicit) findings.push(finding('error', sourceIdentities.length ? 'portable.workspace-landing.workspace-repository-ambiguous' : 'portable.workspace-landing.workspace-repository-missing', 'Requested Workspace must expose exactly one explicit repository identity before it can target a local Git repository.', { workspaceId, count: sourceIdentities.length }));
|
|
63
|
-
return freeze({ ...base, state: 'not-targetable', source: repoSources[0] || null, repository: null, reasons: [sourceIdentities.length ? 'workspace-repository-ambiguous' : 'workspace-repository-missing'] });
|
|
64
|
+
return freeze({ ...base, state: 'not-targetable', source: repoSources[0] || null, repository: null, preflight: unavailablePreflight(sourceIdentities.length ? 'workspace-repository-ambiguous' : 'workspace-repository-missing'), reasons: [sourceIdentities.length ? 'workspace-repository-ambiguous' : 'workspace-repository-missing'] });
|
|
64
65
|
}
|
|
65
66
|
const source = repoSources.find((item) => normalizeRepositoryIdentity(item.repository) === sourceIdentities[0]) || repoSources[0];
|
|
66
67
|
const matches = repositories.filter((repo) => repo.repositoryIdentity === sourceIdentities[0]);
|
|
@@ -72,28 +73,112 @@ function planWorkspace({ workspace = {}, provider = null, repositories = [], sel
|
|
|
72
73
|
if (!selected) {
|
|
73
74
|
if (matches.length > 1) findings.push(finding('error', 'portable.workspace-landing.local-repository-ambiguous', 'Multiple local repositories match the same qualified Workspace repository identity; explicit selection is required.', { workspaceId, repository: source.repository, count: matches.length }));
|
|
74
75
|
else if (explicit || selectionId) findings.push(finding('error', 'portable.workspace-landing.local-repository-unmatched', 'Requested Workspace has no matching local Git repository.', { workspaceId, repository: source.repository }));
|
|
75
|
-
return freeze({ ...base, state: matches.length > 1 ? 'ambiguous' : 'unmatched', source, repository: null, candidateRepositoryIds: matches.map((item) => item.id), reasons: [matches.length > 1 ? 'local-repository-ambiguous' : 'local-repository-unmatched'] });
|
|
76
|
+
return freeze({ ...base, state: matches.length > 1 ? 'ambiguous' : 'unmatched', source, repository: null, preflight: unavailablePreflight(matches.length > 1 ? 'local-repository-ambiguous' : 'local-repository-unmatched'), candidateRepositoryIds: matches.map((item) => item.id), reasons: [matches.length > 1 ? 'local-repository-ambiguous' : 'local-repository-unmatched'] });
|
|
76
77
|
}
|
|
77
78
|
const reasons = [];
|
|
78
|
-
if (selected.clean !== true) {
|
|
79
|
-
reasons.push(selected.clean === false ? 'dirty-worktree' : 'clean-state-unresolved');
|
|
80
|
-
findings.push(finding('error', selected.clean === false ? 'portable.workspace-landing.local-repository-dirty' : 'portable.workspace-landing.local-repository-clean-state-unresolved', 'Landing requires a clean target worktree except ignored local material.', { workspaceId, repositoryId: selected.id }));
|
|
81
|
-
}
|
|
82
79
|
const declaredRef = String(source.ref || '').trim();
|
|
83
80
|
const branch = String(selected.branch || '').trim();
|
|
84
81
|
if (declaredRef && branch && declaredRef !== branch) {
|
|
85
82
|
reasons.push('ref-branch-mismatch');
|
|
86
83
|
findings.push(finding('error', 'portable.workspace-landing.ref-branch-mismatch', 'Local repository branch differs from the qualified Workspace source ref safety constraint.', { workspaceId, repositoryId: selected.id, declaredRef, branch }));
|
|
87
84
|
}
|
|
85
|
+
const preflight = projectExactLandingPreflight(workspaceId, provider, selected, findings);
|
|
86
|
+
if (preflight.state !== 'exact-safe') reasons.push(preflight.state === 'reconciliation-required' ? 'reconciliation-required' : 'target-source-snapshot-unresolved');
|
|
88
87
|
return freeze({
|
|
89
88
|
...base,
|
|
90
89
|
state: reasons.length ? 'blocked' : 'ready',
|
|
91
90
|
source,
|
|
92
|
-
repository: freeze({ id: selected.id, root: selected.root, repository: selected.repository, repositoryIdentity: selected.repositoryIdentity, branch, clean: selected.clean }),
|
|
91
|
+
repository: freeze({ id: selected.id, root: selected.root, repository: selected.repository, repositoryIdentity: selected.repositoryIdentity, branch, clean: selected.clean, sourceSnapshotState: String(selected.sourceSnapshot?.state || '') }),
|
|
92
|
+
preflight,
|
|
93
93
|
reasons
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function projectExactLandingPreflight(workspaceId, provider, selected, findings) {
|
|
98
|
+
const sourceSnapshot = selected.sourceSnapshot;
|
|
99
|
+
if (!sourceSnapshot || String(sourceSnapshot.state || '') !== 'qualified' || !Array.isArray(sourceSnapshot.entries)) {
|
|
100
|
+
findings.push(finding('error', 'portable.workspace-landing.target-source-snapshot-required', 'Landing requires an exact target source-byte snapshot; repository clean state alone cannot prove that replacing the target will preserve newer or divergent work.', { workspaceId, repositoryId: selected.id, clean: selected.clean }));
|
|
101
|
+
return unavailablePreflight('target-source-snapshot-required');
|
|
102
|
+
}
|
|
103
|
+
const incoming = createPortableSourceFrontier({
|
|
104
|
+
id: `landing-incoming:${workspaceId}`,
|
|
105
|
+
source: { kind: 'qualified-handoff-package-workspace', workspaceId },
|
|
106
|
+
workspaces: [{ workspaceId, entries: (provider.entries || []).map(entryIdentity) }]
|
|
107
|
+
});
|
|
108
|
+
const current = createPortableSourceFrontier({
|
|
109
|
+
id: `landing-current:${workspaceId}`,
|
|
110
|
+
source: { kind: 'local-target-source-snapshot', repositoryId: selected.id, root: selected.root },
|
|
111
|
+
workspaces: [{ workspaceId, entries: sourceSnapshot.entries, evidence: sourceSnapshot.evidence || {} }]
|
|
112
|
+
});
|
|
113
|
+
const reconciliation = reconcilePortableSourceFrontiers({ base: incoming, incoming, current });
|
|
114
|
+
const workspace = (reconciliation.workspaces || []).find((item) => String(item.workspaceId || '') === workspaceId) || null;
|
|
115
|
+
if (reconciliation.status !== 'ready' || !workspace || ['qualification-error', 'unavailable', 'locked'].includes(String(workspace.state || ''))) {
|
|
116
|
+
findings.push(finding('error', 'portable.workspace-landing.target-source-snapshot-unqualified', 'Exact target source-byte preflight could not qualify the package/current frontier comparison; landing remains blocked.', { workspaceId, repositoryId: selected.id, reconciliationState: String(workspace?.state || reconciliation.state || '') }));
|
|
117
|
+
return freeze({
|
|
118
|
+
schema: 'tiinex.portable.workspace-landing-preflight.v1',
|
|
119
|
+
state: 'unresolved',
|
|
120
|
+
outcome: 'stop',
|
|
121
|
+
basis: 'accepted-recovery-as-base-and-incoming-vs-exact-current-target',
|
|
122
|
+
counts: freeze({}),
|
|
123
|
+
paths: freeze([]),
|
|
124
|
+
reconciliationState: String(workspace?.state || reconciliation.state || ''),
|
|
125
|
+
boundary: landingPreflightBoundary()
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const paths = (workspace.paths || []).map((item) => freeze({
|
|
129
|
+
path: String(item.path || ''),
|
|
130
|
+
classification: String(item.classification || ''),
|
|
131
|
+
currentChange: String(item.currentChange || ''),
|
|
132
|
+
base: item.base || null,
|
|
133
|
+
incoming: item.incoming || null,
|
|
134
|
+
current: item.current || null
|
|
135
|
+
}));
|
|
136
|
+
if (!paths.length && String(workspace.state || '') === 'exact') {
|
|
137
|
+
return freeze({
|
|
138
|
+
schema: 'tiinex.portable.workspace-landing-preflight.v1',
|
|
139
|
+
state: 'exact-safe',
|
|
140
|
+
outcome: 'land',
|
|
141
|
+
basis: 'accepted-recovery-as-base-and-incoming-vs-exact-current-target',
|
|
142
|
+
counts: workspace.counts || freeze({ currentOnly: 0, conflictCandidate: 0, total: 0 }),
|
|
143
|
+
paths: freeze([]),
|
|
144
|
+
reconciliationState: 'exact',
|
|
145
|
+
boundary: landingPreflightBoundary()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const counts = workspace.counts || {};
|
|
149
|
+
findings.push(finding('error', 'portable.workspace-landing.reconciliation-required', 'Target source diverges from the accepted Recovery snapshot. Landing must stop before mutation and requires explicit reconciliation or preservation of the reported current-side changes.', {
|
|
150
|
+
workspaceId,
|
|
151
|
+
repositoryId: selected.id,
|
|
152
|
+
currentOnly: Number(counts.currentOnly || 0),
|
|
153
|
+
conflictCandidate: Number(counts.conflictCandidate || 0),
|
|
154
|
+
changedPaths: Number(counts.total || paths.length)
|
|
155
|
+
}));
|
|
156
|
+
return freeze({
|
|
157
|
+
schema: 'tiinex.portable.workspace-landing-preflight.v1',
|
|
158
|
+
state: 'reconciliation-required',
|
|
159
|
+
outcome: 'stop',
|
|
160
|
+
basis: 'accepted-recovery-as-base-and-incoming-vs-exact-current-target',
|
|
161
|
+
counts,
|
|
162
|
+
paths: freeze(paths),
|
|
163
|
+
reconciliationState: String(workspace.state || reconciliation.state || ''),
|
|
164
|
+
boundary: landingPreflightBoundary()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function unavailablePreflight(reason = '') {
|
|
169
|
+
return freeze({
|
|
170
|
+
schema: 'tiinex.portable.workspace-landing-preflight.v1',
|
|
171
|
+
state: 'unresolved',
|
|
172
|
+
outcome: 'stop',
|
|
173
|
+
reason: String(reason || ''),
|
|
174
|
+
basis: 'exact-target-source-required',
|
|
175
|
+
counts: freeze({}),
|
|
176
|
+
paths: freeze([]),
|
|
177
|
+
reconciliationState: 'unresolved',
|
|
178
|
+
boundary: landingPreflightBoundary()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
97
182
|
function result(status, inspection, workspaces, findings, input) {
|
|
98
183
|
const affected = workspaces.filter((item) => item.state === 'ready');
|
|
99
184
|
const untouched = workspaces.filter((item) => item.state !== 'ready');
|
|
@@ -109,12 +194,12 @@ function result(status, inspection, workspaces, findings, input) {
|
|
|
109
194
|
required: affected.length > 0,
|
|
110
195
|
repositoryCount: affected.length,
|
|
111
196
|
repositoryRoots: freeze(affected.map((item) => String(item.repository?.root || '')).filter(Boolean)),
|
|
112
|
-
statement: affected.length ? `
|
|
197
|
+
statement: affected.length ? `Exact source preflight passed for ${affected.length} local Git repositor${affected.length === 1 ? 'y' : 'ies'}; replace qualified non-ignored Workspace source, preserve .git and unrelated excluded local material, and do not commit or push.` : '',
|
|
113
198
|
authority: 'explicit-human-host-confirmation-only'
|
|
114
199
|
}),
|
|
115
200
|
operationBoundary: freeze({ sourceMutation: false, remoteWrite: false, commit: false, push: false, acceptance: false }),
|
|
116
201
|
findings: freeze(findings),
|
|
117
|
-
boundary: 'Planning-only projection from one qualified Handoff package plus explicit local Git repository facts. Repository/ref matching
|
|
202
|
+
boundary: 'Planning-only projection from one qualified Handoff package plus explicit local Git repository facts and exact target source-byte snapshots. Repository/ref matching and byte preflight are operational safety constraints, not semantic authority. Clean/dirty Git state alone never authorizes replacement. The plan does not extract, write, commit, push, accept, or complete any Workspace.'
|
|
118
203
|
});
|
|
119
204
|
}
|
|
120
205
|
|
|
@@ -128,10 +213,22 @@ function normalizeRepositories(value = []) {
|
|
|
128
213
|
repository,
|
|
129
214
|
repositoryIdentity: normalizeRepositoryIdentity(repository),
|
|
130
215
|
branch: String(item?.branch || ''),
|
|
131
|
-
clean: item?.clean === true ? true : item?.clean === false ? false : null
|
|
216
|
+
clean: item?.clean === true ? true : item?.clean === false ? false : null,
|
|
217
|
+
sourceSnapshot: normalizeSourceSnapshot(item?.sourceSnapshot || item?.snapshot || null)
|
|
132
218
|
});
|
|
133
219
|
}));
|
|
134
220
|
}
|
|
221
|
+
function normalizeSourceSnapshot(value) {
|
|
222
|
+
if (!value || typeof value !== 'object') return null;
|
|
223
|
+
return freeze({
|
|
224
|
+
state: String(value.state || (Array.isArray(value.entries) ? 'qualified' : '')),
|
|
225
|
+
entries: Array.isArray(value.entries) ? value.entries.map((entry) => entryIdentity(entry)) : [],
|
|
226
|
+
evidence: value.evidence && typeof value.evidence === 'object' ? { ...value.evidence } : {},
|
|
227
|
+
findings: Array.isArray(value.findings) ? value.findings.map((item) => ({ ...item })) : []
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function entryIdentity(entry = {}) { return freeze({ path: String(entry.path || entry.innerPath || ''), bytes: Number(entry.bytes ?? entry.size ?? 0), sha256: String(entry.sha256 || '').toLowerCase() }); }
|
|
231
|
+
function landingPreflightBoundary() { return 'Read-only Recovery landing safety proof. The accepted carried Workspace is used as both base and incoming so every target deviation is current-side divergence; explicit reconciliation remains separate and no mutation or semantic merge is performed.'; }
|
|
135
232
|
function normalizeSelections(value = {}) {
|
|
136
233
|
const source = value?.selections && typeof value.selections === 'object' ? value.selections : value;
|
|
137
234
|
return Object.fromEntries(Object.entries(source || {}).map(([key, selected]) => [normalizeId(key), String(selected || '')]).filter(([key, selected]) => key && selected));
|
|
@@ -7,6 +7,7 @@ import { projectPortableHandoffCarrierOutputFromPackage } from './handoff/recipi
|
|
|
7
7
|
import { orientColdConsumerFromHandoffPackage } from './handoff/coldConsumerEntrypoint.js';
|
|
8
8
|
import { auditHandoffPackageContextCarriage } from './handoff/contextAudit.js';
|
|
9
9
|
import { projectPortableWorkspaceLandingPlan } from './handoff/workspaceLandingPlan.js';
|
|
10
|
+
import { auditPortableRecoveryAcceptance } from './handoff/recoveryAcceptanceAudit.js';
|
|
10
11
|
import { projectPortableEditorAssistance } from './editor/editor.assistance.js';
|
|
11
12
|
import { projectQualifiedHandoffLeaves } from './handoff/handoffLeafProjection.js';
|
|
12
13
|
import { projectPortableAuthoringParent } from './editor/authoring.parent.js';
|
|
@@ -62,11 +63,18 @@ export function createPortablePackageOperationEntries({ operation, wrapPortableR
|
|
|
62
63
|
}),
|
|
63
64
|
'project-workspace-landing': operation({
|
|
64
65
|
name: 'project-workspace-landing',
|
|
65
|
-
description: 'Project qualified carried Workspaces onto explicit local Git repository facts
|
|
66
|
+
description: 'Project qualified carried Workspaces onto explicit local Git repository facts plus exact target source-byte snapshots, failing closed on any target divergence before extraction or mutation.',
|
|
66
67
|
safety: 'planning-only-read-only',
|
|
67
68
|
inputSchema: 'tiinex.portable.workspace-landing-plan.request.v1',
|
|
68
69
|
handler: (input = {}) => wrapPortableResult('project-workspace-landing', projectPortableWorkspaceLandingPlan(input))
|
|
69
70
|
}),
|
|
71
|
+
'audit-recovery-acceptance': operation({
|
|
72
|
+
name: 'audit-recovery-acceptance',
|
|
73
|
+
description: 'Audit one qualified candidate Recovery carrier against one explicit accepted-basis carrier for complete Workspace re-materialization and unexplained source removals without granting acceptance.',
|
|
74
|
+
safety: 'read-only-mechanical-audit',
|
|
75
|
+
inputSchema: 'tiinex.portable.recovery-acceptance-audit.request.v1',
|
|
76
|
+
handler: (input = {}) => wrapPortableResult('audit-recovery-acceptance', auditPortableRecoveryAcceptance(input))
|
|
77
|
+
}),
|
|
70
78
|
'project-editor-assistance': operation({
|
|
71
79
|
name: 'project-editor-assistance',
|
|
72
80
|
description: 'Project exact shared validator findings, deterministic source locations where provable, and bounded deterministic hygiene repairs for editor hosts.',
|