@tiinex/core 0.28.0 → 0.30.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/schemas/creation.contracts.js +6 -2
- package/src/schemas/creation.representation.js +2 -1
- package/src/tooling/portable/adapters/cli/cli.command-input.js +1 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +4 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-sibling-allocation.js +54 -1
- package/src/tooling/portable/adapters/cli/cli.help.js +3 -3
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +1 -1
- package/src/tooling/portable/adapters/node/handoff.manufacture.scope.js +81 -8
- package/src/tooling/portable/draft/draft.exact.js +6 -2
- package/src/tooling/portable/editor/authoring.parent.js +15 -2
- package/src/tooling/portable/grounding/grounding.participantAuthority.js +1 -1
- package/src/tooling/portable/grounding/grounding.readiness.authority.js +7 -1
- package/src/tooling/portable/grounding/grounding.readiness.js +5 -5
- package/src/tooling/portable/handoff/carrierProjection.routeQualification.js +15 -3
- package/src/tooling/portable/handoff/coldStartQualification.grounding.js +84 -56
- package/src/tooling/portable/handoff/coldStartQualification.materials.js +2 -1
- package/src/tooling/portable/handoff/recipientV2.endpointRolePointers.js +14 -1
- package/src/tooling/portable/handoff/recipientV2.inspect.projection.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +2 -2
- package/src/tooling/portable/handoff/recipientV2.topology.js +14 -14
- package/src/tooling/portable/materialization/epistemic.plan.js +21 -7
- package/src/transitions/record.transitions.js +20 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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,
|
|
@@ -173,12 +173,12 @@
|
|
|
173
173
|
"type": "git",
|
|
174
174
|
"url": "git+https://github.com/Tiinex/core.git"
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "5d51273d4c10f8a15367c32bcef8bbce11484ca8",
|
|
177
177
|
"tiinexRelease": {
|
|
178
178
|
"policy": "tiinex.master-npm-release.v1",
|
|
179
|
-
"sourceCommit": "
|
|
180
|
-
"sourceTree": "
|
|
179
|
+
"sourceCommit": "5d51273d4c10f8a15367c32bcef8bbce11484ca8",
|
|
180
|
+
"sourceTree": "1372b29b40dbee5e11d2a52d9cac7731f41941a2",
|
|
181
181
|
"repository": "Tiinex/core",
|
|
182
|
-
"previousVersion": "0.
|
|
182
|
+
"previousVersion": "0.29.0"
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -173,7 +173,10 @@ export function validateArtifactCreationResult(draft = {}, parentRecord = {}, op
|
|
|
173
173
|
|
|
174
174
|
if (rootCreation) findings.push(...validateRootCreationRepresentation(draft.markdown || '', contract));
|
|
175
175
|
else if (parentExpected) {
|
|
176
|
-
const
|
|
176
|
+
const recoveryMode = String(parentRecord?.recoveryMode || parentRecord?.parentRecoveryMode || '').trim();
|
|
177
|
+
const relativeReference = recoveryMode === 'workspace-qualified'
|
|
178
|
+
? String(parentRecord.relativeReference || parentRecord.path || '')
|
|
179
|
+
: relativePath(dirname(options.childPath || draft.path || ''), parentRecord.path || '');
|
|
177
180
|
const parentIntegrityTarget = qualifiedParentIntegrityTarget(parentRecord, relativeReference);
|
|
178
181
|
const representation = qualifyContinuationCreationRepresentation(draft.markdown || '', contract, parentRecord, { relativeReference, parentIntegrityTarget });
|
|
179
182
|
findings.push(...(representation.findings || []).map((message, index) => error(`creation.continuation-representation.${index + 1}`, message)));
|
|
@@ -221,7 +224,8 @@ function qualifiedParentIntegrityTarget(parentRecord = {}, relativeReference = '
|
|
|
221
224
|
const published = parentRecord?.publishedReference || parentRecord?.browseGitReference || parentRecord?.browseGit || null;
|
|
222
225
|
const target = typeof published === 'string' ? '' : String(published?.target || published?.url || '');
|
|
223
226
|
const state = typeof published === 'string' ? 'unresolved' : String(published?.state || published?.resolutionState || 'unresolved');
|
|
224
|
-
const
|
|
227
|
+
const declaredRecoveryMode = String(parentRecord?.recoveryMode || parentRecord?.parentRecoveryMode || '').trim();
|
|
228
|
+
const recoveryMode = declaredRecoveryMode === 'external-versioned' ? 'external-versioned' : declaredRecoveryMode === 'workspace-qualified' ? 'workspace-qualified' : 'local-relative';
|
|
225
229
|
if (recoveryMode === 'external-versioned') return state === 'qualified' && target ? target : '';
|
|
226
230
|
return state === 'qualified' && target ? target : String(relativeReference || '');
|
|
227
231
|
}
|
|
@@ -217,7 +217,8 @@ export function qualifyContinuationCreationRepresentation(markdown = '', contrac
|
|
|
217
217
|
const publishedTarget = typeof publishedReference === 'string' ? '' : String(publishedReference?.target || publishedReference?.url || '');
|
|
218
218
|
const publishedState = typeof publishedReference === 'string' ? 'unresolved' : String(publishedReference?.state || publishedReference?.resolutionState || 'unresolved');
|
|
219
219
|
const publishedQualified = Boolean(publishedTarget && publishedState === 'qualified');
|
|
220
|
-
const
|
|
220
|
+
const declaredRecoveryMode = String(parentRecord?.recoveryMode || parentRecord?.parentRecoveryMode || '').trim();
|
|
221
|
+
const recoveryMode = declaredRecoveryMode === 'external-versioned' ? 'external-versioned' : declaredRecoveryMode === 'workspace-qualified' ? 'workspace-qualified' : 'local-relative';
|
|
221
222
|
if (recoveryMode === 'external-versioned') {
|
|
222
223
|
if (!publishedQualified) findings.push('External Parent recovery requires one qualified version-stable published representation.');
|
|
223
224
|
if (origins.length !== 1 || relative.length !== 0 || browse.length !== 1) findings.push('External Parent Origin must contain exactly one [browse + git](...) entry and must not fabricate [relative](...).');
|
|
@@ -182,6 +182,7 @@ export async function commandInput(parsed, runtime = {}) {
|
|
|
182
182
|
options: {}
|
|
183
183
|
};
|
|
184
184
|
if(parsed.command==='project-workspace-landing')return land(flags,material,readOptionalJson,splitFlag);
|
|
185
|
+
if (parsed.command === 'project-authoring-parent') return { input: { ...material, reference: flags.reference || '' }, options: {} };
|
|
185
186
|
const operatorBridgeInput = await prepareOperatorBridgeCliInput(parsed.command, material, flags, readOptionalJson);
|
|
186
187
|
if (operatorBridgeInput) return operatorBridgeInput;
|
|
187
188
|
if (parsed.command === 'project-editor-assistance') return prepareEditorAssistanceCliInput(material, flags);
|
|
@@ -20,6 +20,7 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
20
20
|
if (!['handoff', 'workspace', 'bootstrap'].includes(carrierMode)) throw new Error(`portable.cli.handoff-carrier.carrier-mode.invalid:${carrierMode}`);
|
|
21
21
|
if (carrierMode === 'workspace') return prepareWorkspaceCarrierCliCommand(flags, workspaceRoot, runtime);
|
|
22
22
|
if (carrierMode === 'bootstrap') return prepareBootstrapCarrierCliCommand(flags, runtime);
|
|
23
|
+
if (flags['package-major'] && flags['package-consolidation']) throw new Error('portable.cli.handoff-carrier.package-major-consolidation-conflict');
|
|
23
24
|
const continuationState = parsed.surfaceCommand === 'handoff'
|
|
24
25
|
? await readGroundContinuationState(workspaceRoot)
|
|
25
26
|
: {};
|
|
@@ -103,6 +104,7 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
103
104
|
selectedRoutePointer: continuationState.selectedRoutePointer || flags['package-parent-route-pointer'] || '',
|
|
104
105
|
selectedRouteId: continuationState.selectedRouteId || flags['package-parent-route-id'] || '',
|
|
105
106
|
explicitSiblingIndex: flags['package-sibling-index'],
|
|
107
|
+
consolidation: Boolean(flags['package-consolidation']),
|
|
106
108
|
enabled: Boolean(flags.output || flags['output-dir'])
|
|
107
109
|
});
|
|
108
110
|
carrierAllocation = siblingAllocation;
|
|
@@ -118,6 +120,8 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
118
120
|
packageParentSha256 = String(carrierLineage.parentPackageSha256 || '');
|
|
119
121
|
} else if (flags['package-major']) {
|
|
120
122
|
throw new Error('portable.cli.handoff-carrier.package-major.parent-required');
|
|
123
|
+
} else if (flags['package-consolidation']) {
|
|
124
|
+
throw new Error('portable.cli.handoff-carrier.package-consolidation.parent-required');
|
|
121
125
|
}
|
|
122
126
|
const carrierProfile = selectCarrierProfile({
|
|
123
127
|
operator: operatorCarrierProfile,
|
|
@@ -97,10 +97,62 @@ export function deriveHandoffSiblingAllocation({ parentInspection = null, select
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function deriveHandoffConsolidationAllocation({ parentInspection = null, explicitSiblingIndex = null, parentPackagePath = '', parentPackageSha256 = '', parentDimension = '' } = {}) {
|
|
101
|
+
const explicit = normalizeSiblingIndex(explicitSiblingIndex);
|
|
102
|
+
const inspection = parentInspection && typeof parentInspection === 'object' ? parentInspection : null;
|
|
103
|
+
if (!inspection || inspection.detected === false) return freeze({
|
|
104
|
+
state: 'unavailable', siblingIndex: null, allocationMode: 'unavailable',
|
|
105
|
+
reasonCode: 'qualified-parent-route-topology-unavailable-for-consolidation',
|
|
106
|
+
provenance: provenanceBase({ parentPackagePath, parentPackageSha256, parentDimension, explicitSiblingIndex: explicit }),
|
|
107
|
+
boundary: consolidationBoundary()
|
|
108
|
+
});
|
|
109
|
+
if (String(inspection.status || '') !== 'valid') return consolidationBlocked('qualified-parent-route-topology-invalid-for-consolidation');
|
|
110
|
+
const routes = [...(inspection.routes || [])];
|
|
111
|
+
if (!routes.length) return consolidationBlocked('qualified-parent-route-topology-empty-for-consolidation');
|
|
112
|
+
|
|
113
|
+
const topology = deriveHandoffSiblingAllocation({
|
|
114
|
+
parentInspection: inspection,
|
|
115
|
+
selectedRoutePointer: String(routes[0]?.pointerPath || ''),
|
|
116
|
+
parentPackagePath, parentPackageSha256, parentDimension
|
|
117
|
+
});
|
|
118
|
+
if (topology.state !== 'qualified') return consolidationBlocked(topology.reasonCode || 'qualified-parent-route-pointer-order-unresolved');
|
|
119
|
+
const routeCount = Number(topology.provenance?.qualifiedRouteCount || 0);
|
|
120
|
+
const siblingIndex = routeCount + 1;
|
|
121
|
+
if (!Number.isInteger(siblingIndex) || siblingIndex < 2 || siblingIndex > MAX_SIBLING_INDEX) return consolidationBlocked('derived-consolidation-sibling-index-out-of-range');
|
|
122
|
+
if (explicit && explicit !== siblingIndex) return consolidationBlocked('explicit-sibling-index-conflicts-with-qualified-consolidation-topology', { expectedSiblingIndex: siblingIndex });
|
|
123
|
+
|
|
124
|
+
return freeze({
|
|
125
|
+
state: 'qualified', siblingIndex,
|
|
126
|
+
childDimension: parentDimension ? `${String(parentDimension).trim()}-${siblingIndex}` : '',
|
|
127
|
+
allocationMode: 'qualified-parent-route-consolidation-ordinal',
|
|
128
|
+
explicitOverride: explicit ? 'matched-derived-value' : 'not-supplied',
|
|
129
|
+
reasonCode: '',
|
|
130
|
+
provenance: {
|
|
131
|
+
...provenanceBase({ parentPackagePath, parentPackageSha256, parentDimension, explicitSiblingIndex: explicit }),
|
|
132
|
+
basis: 'qualified-parent-route-consolidation-ordinal',
|
|
133
|
+
qualifiedRouteCount: routeCount,
|
|
134
|
+
consolidationOrdinal: siblingIndex,
|
|
135
|
+
commonFrontierDimension: String(parentDimension || '').trim(),
|
|
136
|
+
pointerOrder: topology.provenance.pointerOrder
|
|
137
|
+
},
|
|
138
|
+
boundary: consolidationBoundary()
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
function consolidationBlocked(reasonCode, extra = {}) {
|
|
142
|
+
return freeze({
|
|
143
|
+
state: 'blocked', siblingIndex: null, allocationMode: 'blocked', reasonCode, ...extra,
|
|
144
|
+
provenance: provenanceBase({ parentPackagePath, parentPackageSha256, parentDimension, explicitSiblingIndex: explicit }),
|
|
145
|
+
boundary: consolidationBoundary()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
export async function resolveHandoffSiblingAllocation(input = {}) {
|
|
101
|
-
const
|
|
151
|
+
const consolidation = input.consolidation === true;
|
|
152
|
+
const derived = consolidation ? deriveHandoffConsolidationAllocation(input) : deriveHandoffSiblingAllocation(input);
|
|
102
153
|
if (derived.state === 'qualified') return derived;
|
|
103
154
|
if (derived.state === 'blocked') throw new Error(`portable.cli.handoff-carrier.sibling-allocation.${derived.reasonCode}`);
|
|
155
|
+
if (consolidation) throw new Error('portable.cli.handoff-carrier.sibling-allocation.qualified-parent-route-topology-required-for-consolidation');
|
|
104
156
|
const explicit = normalizeSiblingIndex(input.explicitSiblingIndex ?? input.siblingIndex);
|
|
105
157
|
if (!explicit) throw new Error('portable.cli.handoff-carrier.sibling-allocation.explicit-index-required-when-topology-unavailable');
|
|
106
158
|
const legacy = await reserveHandoffSiblingIndex({
|
|
@@ -172,6 +224,7 @@ function provenanceBase({ parentPackagePath = '', parentPackageSha256 = '', pare
|
|
|
172
224
|
explicitSiblingIndex: explicitSiblingIndex ? normalizeSiblingIndex(explicitSiblingIndex) : null
|
|
173
225
|
};
|
|
174
226
|
}
|
|
227
|
+
function consolidationBoundary() { return 'Transport-only carrier consolidation allocation. The consolidation sibling is exactly N+1 for N qualified Handoff routes on the common pre-batch carrier frontier. It is never derived from one specialist return, arrival order, retries, output collisions, local allocation files or artifact Parent lineage; explicit Major stabilization remains separate.'; }
|
|
175
228
|
function allocationBoundary() { return 'Transport-only carrier allocation. A non-Major sibling ordinal may come only from exact qualified package-local Handoff Pointer order for the selected parent route, or from an explicit advanced override when such topology is unavailable. Allocation never creates semantic Parent, Workspace, Role, acceptance, completion, participant, process, or source authority; different carrier prefixes are not coordinated.'; }
|
|
176
229
|
function normalizeSiblingIndex(value) {
|
|
177
230
|
if (value === null || value === undefined || value === '') return 0;
|
|
@@ -63,9 +63,9 @@ function commonCommandHelp(command, surfaceCommand) {
|
|
|
63
63
|
`${command} ground <handoff-package.zip> --route <Continue-from> [--holder-role <recipient-role>]`,
|
|
64
64
|
`${command} ground <handoff-package.zip> --route <Continue-from> --holder-role <recipient-role> --continue <workspace-dir>`,
|
|
65
65
|
'',
|
|
66
|
-
'Reads and qualifies the exact selected Handoff route. The default projection keeps readiness, recipient authority boundary,
|
|
66
|
+
'Reads and qualifies the exact selected Handoff route. The default projection keeps readiness, recipient authority boundary, consuming-session holder binding, Required Context closure, continuity/blockers, current Task identity, and exact next action compact; add `--full` for the full qualified receipt.',
|
|
67
67
|
'Add `--include-required-context <requirement-id,name|all>` and/or `--include-current-work` only when exact body text is needed. `ground --continue` includes the bounded current Task body needed to proceed, retains Required Context counts and continuity/recovery state, and does not repeat qualified Required Context item paths or root-detail receipts unless explicitly requested (or `--full` is used).',
|
|
68
|
-
'For a Role recipient, `--holder-role <recipient-role>`
|
|
68
|
+
'For a Role recipient, exact qualified consumption of the selected Handoff may establish the bounded consuming-session Role-capacity binding when the exact qualified recipient Role authorizes canonical Assignment Mode `handoff`. `--holder-role <recipient-role>` remains an explicit consuming-session assertion and must match the selected recipient; mismatches block and never fall back to Handoff assignment. No binding is inferred from package delivery, route orientation alone, provider identity, or assistant/user position. After `grounded-to-act`, `--continue` materializes the selected carried Workspace into an empty local directory and writes runtime-only `.tiinex/continuation.json`. The grounding operation itself is non-mutating; downstream work authority comes from qualified Handoff/Task/Role artifacts, not from that operation-safety fact.',
|
|
69
69
|
'',
|
|
70
70
|
`Advanced/internal catalog: ${command} operations`
|
|
71
71
|
];
|
|
@@ -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. Ordinary non-Major continuation derives its sibling ordinal from the exact qualified selected Handoff Pointer order in the received parent carrier, so common-path `handoff` does not require `--package-sibling-index` or `--return-package-sibling-index`. A single selected Pointer derives `-1`; parallel qualified Pointers derive dense local ordinals in Pointer order, and each returned branch continues from its own carrier. `--package-sibling-index` remains an advanced compatibility override only when qualified route topology is unavailable, and must match when topology already proves the value; conflicting or ambiguous topology fails closed. `--return-package-major` remains the explicit Major-return declaration. Different carrier prefixes are never globally coordinated. 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. Ordinary non-Major continuation derives its sibling ordinal from the exact qualified selected Handoff Pointer order in the received parent carrier, so common-path `handoff` does not require `--package-sibling-index` or `--return-package-sibling-index`. A single selected Pointer derives `-1`; parallel qualified Pointers derive dense local ordinals in Pointer order, and each returned branch continues from its own carrier. Advanced manufacture may declare `--package-consolidation` against the common pre-batch parent carrier; with N qualified route Pointers it derives consolidation ordinal N+1 and rejects conflicting overrides or missing qualified common-frontier topology. `--package-sibling-index` remains an advanced compatibility override only when qualified route topology is unavailable for ordinary route returns, and must match when topology already proves the value; conflicting or ambiguous topology fails closed. `--return-package-major` remains the explicit Major-return declaration. Different carrier prefixes are never globally coordinated. 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
|
];
|
|
@@ -181,7 +181,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
181
181
|
const dependencyClosure = await expandPointerDependencyClosure({ requirements, materials, workspaceRuntimeById, bindings: input.materialBindings || {} });
|
|
182
182
|
requirements = dependencyClosure.requirements;
|
|
183
183
|
materials = appendMissingRequirementMaterials(dependencyClosure.materials, resolvePackageParentRequirementMaterials(requirements, packageParentExactMaterialProvider));
|
|
184
|
-
const routeParentBoundaryClosure = expandRouteParentBoundaryClosure({ requirements, materials, workspaceMaterializations, workspaceRuntimeById, routeSpecs });
|
|
184
|
+
const routeParentBoundaryClosure = expandRouteParentBoundaryClosure({ requirements, materials, workspaceMaterializations, workspaceRuntimeById, routeSpecs, exactMaterialProvider: packageParentExactMaterialProvider });
|
|
185
185
|
requirements = routeParentBoundaryClosure.requirements;
|
|
186
186
|
materials = appendMissingRequirementMaterials(routeParentBoundaryClosure.materials, resolvePackageParentRequirementMaterials(requirements, packageParentExactMaterialProvider));
|
|
187
187
|
const parentBoundaryClosure = expandBoundedParentBoundaryClosure({ requirements, materials, workspaceMaterializations, workspaceRuntimeById });
|
|
@@ -166,10 +166,9 @@ export function expandRouteParentBoundaryClosure(input = {}) {
|
|
|
166
166
|
const visitKey = `${currentWorkspaceId}\0${currentPath}`;
|
|
167
167
|
if (visited.has(visitKey)) break;
|
|
168
168
|
visited.add(visitKey);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const markdown = decodeUtf8(currentEntry.data);
|
|
169
|
+
const currentSource = exactArtifactSource(input, materials, currentWorkspaceId, currentPath);
|
|
170
|
+
if (!currentSource) break;
|
|
171
|
+
const markdown = decodeUtf8(currentSource.data);
|
|
173
172
|
if (!markdown) break;
|
|
174
173
|
let parent;
|
|
175
174
|
try { parent = parseArtifactMarkdown(markdown).envelope?.parent || {}; } catch { break; }
|
|
@@ -188,8 +187,7 @@ export function expandRouteParentBoundaryClosure(input = {}) {
|
|
|
188
187
|
}
|
|
189
188
|
if (!targetWorkspaceId || !targetPath) break;
|
|
190
189
|
|
|
191
|
-
const
|
|
192
|
-
const targetEntry = targetRuntime ? entryFromEnumeration(targetRuntime.enumeration, targetPath) : null;
|
|
190
|
+
const targetSource = exactArtifactSource(input, materials, targetWorkspaceId, targetPath);
|
|
193
191
|
const targetMaterialization = materializationByWorkspaceId.get(targetWorkspaceId);
|
|
194
192
|
const targetAlreadyCarried = workspaceMaterializationIncludesPath(targetMaterialization, targetPath);
|
|
195
193
|
const routeTargetKey = `${routeWorkspaceId}\0${routePath}\0${targetWorkspaceId}\0${targetPath}`;
|
|
@@ -217,10 +215,10 @@ export function expandRouteParentBoundaryClosure(input = {}) {
|
|
|
217
215
|
fields: Object.freeze({ RouteWorkspace: routeWorkspaceId, RoutePath: routePath, SourceWorkspace: currentWorkspaceId, SourcePath: currentPath, TargetWorkspace: targetWorkspaceId, TargetPath: targetPath, Reference: reference })
|
|
218
216
|
});
|
|
219
217
|
dependencies.push(requirement);
|
|
220
|
-
if (
|
|
218
|
+
if (targetSource) materials.push(materialCandidateFromExactArtifactSource(requirement, targetSource));
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
|
-
if (!
|
|
221
|
+
if (!targetSource) break;
|
|
224
222
|
currentWorkspaceId = targetWorkspaceId;
|
|
225
223
|
currentPath = targetPath;
|
|
226
224
|
}
|
|
@@ -232,6 +230,81 @@ export function expandRouteParentBoundaryClosure(input = {}) {
|
|
|
232
230
|
});
|
|
233
231
|
}
|
|
234
232
|
|
|
233
|
+
function exactArtifactSource(input = {}, materials = [], workspaceIdValue = '', targetPathValue = '') {
|
|
234
|
+
const workspaceId = String(workspaceIdValue || '').trim();
|
|
235
|
+
const targetPath = normalizeRelativePath(targetPathValue);
|
|
236
|
+
if (!workspaceId || !targetPath) return null;
|
|
237
|
+
|
|
238
|
+
// Explicit current/inherited Workspace providers retain precedence over detached cache
|
|
239
|
+
// material. This preserves the existing package-parent precedence boundary while allowing
|
|
240
|
+
// the exact same route traversal to continue when the next ancestor is cache-only.
|
|
241
|
+
const runtime = input.workspaceRuntimeById?.get(workspaceId);
|
|
242
|
+
const entry = runtime ? entryFromEnumeration(runtime.enumeration, targetPath) : null;
|
|
243
|
+
if (entry) return Object.freeze({
|
|
244
|
+
kind: 'workspace-entry', workspaceId, path: targetPath, data: entry.data, bytes: Number(entry.bytes || 0), sha256: String(entry.sha256 || ''),
|
|
245
|
+
mediaType: String(entry.mediaType || 'text/markdown'), entry, runtime
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const candidates = [];
|
|
249
|
+
for (const material of materials || []) {
|
|
250
|
+
const provenance = material?.provenance || {};
|
|
251
|
+
const candidateWorkspaceId = String(provenance.workspaceId || material.workspaceId || material.targetWorkspaceId || '').trim();
|
|
252
|
+
const candidatePath = normalizeRelativePath(provenance.path || material.path || material.targetPath || '');
|
|
253
|
+
if (candidateWorkspaceId !== workspaceId || candidatePath !== targetPath || !material?.data) continue;
|
|
254
|
+
candidates.push(Object.freeze({
|
|
255
|
+
kind: 'qualified-material', workspaceId, path: targetPath, data: material.data, bytes: Number(material.bytes || 0), sha256: String(material.sha256 || ''),
|
|
256
|
+
mediaType: String(material.mediaType || 'text/markdown'), providerId: String(material.providerId || ''), providerKind: String(material.providerKind || ''),
|
|
257
|
+
provenance: Object.freeze({ ...provenance }), authority: Object.freeze({ ...(material.authority || {}) })
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
for (const material of input.exactMaterialProvider?.entries || []) {
|
|
261
|
+
const provenance = material?.provenance || {};
|
|
262
|
+
const candidateWorkspaceId = String(provenance.workspaceId || '').trim();
|
|
263
|
+
const candidatePath = normalizeRelativePath(provenance.path || '');
|
|
264
|
+
if (candidateWorkspaceId !== workspaceId || candidatePath !== targetPath || !material?.data) continue;
|
|
265
|
+
candidates.push(Object.freeze({
|
|
266
|
+
kind: 'qualified-material', workspaceId, path: targetPath, data: material.data, bytes: Number(material.bytes || 0), sha256: String(material.sha256 || ''),
|
|
267
|
+
mediaType: String(material.mediaType || 'text/markdown'), providerId: String(material.providerId || 'qualified-package-parent-exact-material'),
|
|
268
|
+
providerKind: String(material.providerKind || 'qualified-package-parent-cache-material'), provenance: Object.freeze({ ...provenance }),
|
|
269
|
+
authority: Object.freeze({ packageParentMaterialQualified: true, semanticAuthority: 'none', sourceSelectionAuthority: false })
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
if (!candidates.length) return null;
|
|
273
|
+
const digests = [...new Set(candidates.map((candidate) => String(candidate.sha256 || '')).filter(Boolean))];
|
|
274
|
+
if (digests.length !== 1) return null;
|
|
275
|
+
return candidates.find((candidate) => candidate.sha256 === digests[0]) || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function materialCandidateFromExactArtifactSource(requirement = {}, source = {}) {
|
|
279
|
+
if (source.kind === 'workspace-entry') {
|
|
280
|
+
return materialCandidateFromWorkspaceEntry(requirement, source.workspaceId, source.path, source.entry, source.runtime.enumeration, source.runtime);
|
|
281
|
+
}
|
|
282
|
+
return Object.freeze({
|
|
283
|
+
requirementId: String(requirement.id || ''),
|
|
284
|
+
referenceTarget: String(requirement.reference?.target || requirement.referenceTarget || ''),
|
|
285
|
+
path: String(source.path || ''),
|
|
286
|
+
data: source.data,
|
|
287
|
+
bytes: Number(source.bytes || 0),
|
|
288
|
+
sha256: String(source.sha256 || ''),
|
|
289
|
+
mediaType: String(source.mediaType || 'text/markdown'),
|
|
290
|
+
providerId: String(source.providerId || 'qualified-exact-material'),
|
|
291
|
+
providerKind: String(source.providerKind || 'qualified-exact-material'),
|
|
292
|
+
provenance: Object.freeze({
|
|
293
|
+
...(source.provenance || {}),
|
|
294
|
+
workspaceId: String(source.workspaceId || source.provenance?.workspaceId || ''),
|
|
295
|
+
path: String(source.path || source.provenance?.path || ''),
|
|
296
|
+
reboundRequirementId: String(requirement.id || ''),
|
|
297
|
+
reboundClassification: String(requirement.classification || '')
|
|
298
|
+
}),
|
|
299
|
+
authority: Object.freeze({
|
|
300
|
+
...(source.authority || {}),
|
|
301
|
+
localIdentityQualified: true,
|
|
302
|
+
semanticAuthority: 'none',
|
|
303
|
+
sourceSelectionAuthority: false
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
235
308
|
function workspaceMaterializationIncludesPath(materialization = null, targetPath = '') {
|
|
236
309
|
if (!materialization) return false;
|
|
237
310
|
const state = String(materialization.state || materialization.materialization || '');
|
|
@@ -16,6 +16,7 @@ export function normalizePortableParentRecord(parent = {}) {
|
|
|
16
16
|
boundary: String(parent.boundary || parent.source?.boundary || ''),
|
|
17
17
|
sourceMode: String(parent.sourceMode || ''),
|
|
18
18
|
recoveryMode: String(parent.recoveryMode || parent.parentRecoveryMode || ''),
|
|
19
|
+
relativeReference: String(parent.relativeReference || ''),
|
|
19
20
|
source: parent.source || null,
|
|
20
21
|
markdown: String(parent.markdown || ''),
|
|
21
22
|
integrity: parent.integrity || null,
|
|
@@ -108,11 +109,14 @@ export function qualifyPortableRenderedParentRepresentation(markdown = '', paren
|
|
|
108
109
|
const observed = parsed.envelope?.parent || {};
|
|
109
110
|
const hasObserved = Boolean(observed.schema?.id || observed.trace || observed.origin || observed.createdAt || observed.boundary || observed.originEntries?.length);
|
|
110
111
|
if (rootCreation) return Object.freeze({ state: hasObserved ? 'invalid' : 'qualified', reason: hasObserved ? 'exact-result-parent-unexpected' : '' });
|
|
111
|
-
const relative = relativePath(dirname(childPath), parent.path);
|
|
112
112
|
const published = parent.publishedReference || {};
|
|
113
113
|
const publishedTarget = String(published.target || '');
|
|
114
114
|
const publishedQualified = Boolean(publishedTarget && published.state === 'qualified');
|
|
115
|
-
const
|
|
115
|
+
const declaredRecoveryMode = String(parent.recoveryMode || '').trim();
|
|
116
|
+
const recoveryMode = declaredRecoveryMode === 'external-versioned' ? 'external-versioned' : declaredRecoveryMode === 'workspace-qualified' ? 'workspace-qualified' : 'local-relative';
|
|
117
|
+
const relative = recoveryMode === 'workspace-qualified'
|
|
118
|
+
? String(parent.relativeReference || parent.path || '')
|
|
119
|
+
: relativePath(dirname(childPath), parent.path);
|
|
116
120
|
const schemaTarget = String(parent.schemaReferenceAuthority?.preferredTarget || '');
|
|
117
121
|
const origins = observed.originEntries || [];
|
|
118
122
|
const createdAtLines = renderedParentFieldLines(markdown, 'Created At');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseArtifactMarkdown } from '../../../artifacts/artifact.parse.js';
|
|
2
2
|
import { auditPortableRecord } from '../audit/audit.capability.js';
|
|
3
|
+
import { classifyParentRecoveryReference } from '../../../lineage/parentRecoveryReference.js';
|
|
4
|
+
import { buildArtifactCreationContract } from '../../../schemas/creation.contracts.js';
|
|
3
5
|
|
|
4
6
|
export const PORTABLE_AUTHORING_PARENT_SCHEMA_ID = 'tiinex.portable.authoring-parent.v1';
|
|
5
7
|
|
|
@@ -15,13 +17,24 @@ export function projectPortableAuthoringParent(input = {}) {
|
|
|
15
17
|
const schemaId = String(parsed.envelope?.current?.schema?.id || audit.schemaId || '');
|
|
16
18
|
const schemaTarget = String(parsed.envelope?.current?.schema?.target || '');
|
|
17
19
|
const createdAt = String(parsed.envelope?.current?.createdAt || audit.artifact?.createdAt || '');
|
|
20
|
+
const explicitReference = String(input.reference || '').trim();
|
|
21
|
+
const projectedPath = explicitReference || String(record.path || record.id || '');
|
|
22
|
+
const referenceClassification = classifyParentRecoveryReference(projectedPath);
|
|
23
|
+
if (referenceClassification.kind === 'malformed-workspace-qualified') return freeze({ schema: PORTABLE_AUTHORING_PARENT_SCHEMA_ID, status: 'blocked', parentRecord: null, findings: [{ severity: 'error', code: 'portable.authoring-parent.reference.malformed', message: 'Selected Parent reference is malformed and cannot be used for native authoring.' }], operationBoundary: boundary() });
|
|
24
|
+
const recoveryMode = referenceClassification.kind === 'workspace-qualified' ? 'workspace-qualified' : 'local-relative';
|
|
25
|
+
const canonicalCurrent = buildArtifactCreationContract({ schemaId, transitionType: 'continue-from-record' })?.schemaReferences?.current || null;
|
|
26
|
+
const canonicalTargets = new Set([...(canonicalCurrent?.exactTargets || []), String(canonicalCurrent?.preferredTarget || '')].filter(Boolean));
|
|
27
|
+
const schemaReferenceQualified = Boolean(schemaTarget && canonicalCurrent?.resolutionState === 'qualified' && canonicalTargets.has(schemaTarget));
|
|
28
|
+
const schemaReferenceAuthority = schemaReferenceQualified
|
|
29
|
+
? { ...canonicalCurrent, preferredTarget: schemaTarget, resolutionState: 'qualified', resolutionEvidence: { ...(canonicalCurrent?.resolutionEvidence || {}), basis: 'declared-parent-target-exact-canonical-match' } }
|
|
30
|
+
: { schemaId, preferredTarget: schemaTarget, exactTargets: schemaTarget ? [schemaTarget] : [], resolutionState: 'unresolved', evidence: { basis: 'declared-current-schema-reference-only' } };
|
|
18
31
|
return freeze({
|
|
19
32
|
schema: PORTABLE_AUTHORING_PARENT_SCHEMA_ID,
|
|
20
33
|
status: 'ready',
|
|
21
34
|
parentRecord: {
|
|
22
|
-
id:
|
|
35
|
+
id: projectedPath, path: projectedPath, schemaId, currentSchemaId: schemaId, currentCreatedAt: createdAt, createdAt, recoveryMode, relativeReference: recoveryMode === 'workspace-qualified' ? projectedPath : '',
|
|
23
36
|
markdown: record.markdown, sourceMode: String(record.sourceMode || 'portable-node-local'),
|
|
24
|
-
schemaReferenceAuthority
|
|
37
|
+
schemaReferenceAuthority
|
|
25
38
|
},
|
|
26
39
|
findings: [],
|
|
27
40
|
operationBoundary: boundary(),
|
|
@@ -14,7 +14,7 @@ export function projectParticipantAuthority(authority = null) {
|
|
|
14
14
|
participantIdentityCreatesAuthority: false,
|
|
15
15
|
conversationPositionCreatesAuthority: false,
|
|
16
16
|
universalHumanFeedbackRule: false,
|
|
17
|
-
boundary: 'Qualified Role material plus
|
|
17
|
+
boundary: 'Qualified Role material plus a qualified holder binding is the basis; that binding may be explicit or may come from exact qualified selected-Handoff consumption when the Role authorizes `handoff`. Identity/chat position alone create no authority; no universal human-input-as-feedback rule is imposed.'
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
function compact(value = '', limit = 180) { const text = String(value || '').replace(/\s+/g, ' ').trim(); return text.length > limit ? `${text.slice(0, limit - 1).trimEnd()}…` : text; }
|
|
@@ -55,12 +55,18 @@ export function projectGroundingAuthority(authority, mode) {
|
|
|
55
55
|
explicit: Boolean(authority.holderBinding.explicit),
|
|
56
56
|
inferredFromTransport: Boolean(authority.holderBinding.inferredFromTransport),
|
|
57
57
|
provenance: Object.freeze({
|
|
58
|
-
basis: authority.holderBinding.explicit ? 'explicit-consuming-session-holder-binding' : 'unresolved-or-non-explicit-holder-binding',
|
|
58
|
+
basis: authority.holderBinding.explicit ? 'explicit-consuming-session-holder-binding' : (authority.holderBinding.source === 'qualified-selected-handoff-consumption' && String(authority.holderBinding.state || '') === 'qualified' ? 'qualified-selected-handoff-consumption-binding' : 'unresolved-or-non-explicit-holder-binding'),
|
|
59
59
|
source: authority.holderBinding.source || '',
|
|
60
60
|
sourceKind: authority.holderBinding.sourceDetail?.kind || '',
|
|
61
61
|
sourceLocator: authority.holderBinding.sourceDetail?.locator || '',
|
|
62
62
|
semanticAuthorityState: authority.holderBinding.sourceDetail?.semanticAuthorityState || 'not-established',
|
|
63
63
|
qualifiedMaterialSource: Boolean(authority.holderBinding.sourceDetail?.qualifiedMaterialSource),
|
|
64
|
+
assignmentMode: authority.holderBinding.assertionMode || authority.holderBinding.authorization?.assignmentMode || '',
|
|
65
|
+
routePointerPath: authority.holderBinding.sourceDetail?.routePointerPath || '',
|
|
66
|
+
handoffArtifactPath: authority.holderBinding.sourceDetail?.handoffArtifactPath || '',
|
|
67
|
+
handoffArtifactSha256: authority.holderBinding.sourceDetail?.handoffArtifactSha256 || '',
|
|
68
|
+
roleArtifactPath: authority.holderBinding.sourceDetail?.roleArtifactPath || authority.holderBinding.authorization?.provenance?.roleArtifactPath || '',
|
|
69
|
+
roleArtifactSha256: authority.holderBinding.sourceDetail?.roleArtifactSha256 || authority.holderBinding.authorization?.provenance?.roleArtifactSha256 || '',
|
|
64
70
|
authorizationState: authority.holderBinding.authorization?.state || 'unresolved',
|
|
65
71
|
authorizationBasis: authority.holderBinding.authorization?.provenance?.basis || '',
|
|
66
72
|
boundary: authority.holderBinding.boundary || 'Consuming-session holder identity is never inferred from route transport or recipient position.'
|
|
@@ -129,7 +129,7 @@ export function composeGroundingReadiness({ mode = 'loaded-material', authority
|
|
|
129
129
|
} else {
|
|
130
130
|
holderBindingActReady = false;
|
|
131
131
|
unresolved.push(evidence('session-holder-role-binding', 'unresolved', `recipient Role ${authority?.role?.endpoint?.label || authority?.handoff?.to || 'recipient'} is qualified separately from the consuming session`));
|
|
132
|
-
reasons.push(reason('session-holder-role-binding-unresolved', 'The selected recipient Role
|
|
132
|
+
reasons.push(reason('session-holder-role-binding-unresolved', 'The selected recipient Role is not yet bound to this consuming session. Exact qualified selected-Handoff consumption may bind it only when the exact recipient Role authorizes Assignment Mode `handoff`; otherwise supply an explicit matching session holder Role binding authorized by that Role.'));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (holderState === 'not-applicable') {
|
|
@@ -142,7 +142,7 @@ export function composeGroundingReadiness({ mode = 'loaded-material', authority
|
|
|
142
142
|
} else {
|
|
143
143
|
holderBindingActReady = false;
|
|
144
144
|
unresolved.push(evidence('session-holder-role-binding-authorization', holderAuthorizationState || 'unresolved', authority?.holderBinding?.authorization?.reasonCode || 'exact qualified canonical assignment-mode authority does not authorize the asserted binding mechanism'));
|
|
145
|
-
reasons.push(reason('session-holder-role-binding-authorization-unresolved', 'The
|
|
145
|
+
reasons.push(reason('session-holder-role-binding-authorization-unresolved', 'The consuming-session Role binding matches the selected recipient Role, but exact qualified canonical assignment-mode authority does not authorize the asserted mechanism. Matching session input or Handoff selection alone cannot make the route act-ready.'));
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -368,12 +368,12 @@ function projectCurrentWork(topology = {}, records = [], includeCurrentWork = fa
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function nextActionFor(state, topology, continuity = {}, authority = null) {
|
|
371
|
-
if (state === 'grounded-to-act') return Object.freeze({ kind: 'continue-bounded-handoff-work', target: topology.currentFrontier[0]?.path || '', basis: 'qualified authority +
|
|
372
|
-
if (state === 'grounded-to-discuss' && String(authority?.holderBinding?.state || 'unresolved') === 'unresolved') return Object.freeze({ kind: '
|
|
371
|
+
if (state === 'grounded-to-act') return Object.freeze({ kind: 'continue-bounded-handoff-work', target: topology.currentFrontier[0]?.path || '', basis: 'qualified authority + qualified session holder Role binding (explicit or exact selected-Handoff consumption) + exact qualified holder-assignment authorization when Role-recipient + required context + cold-start root continuity + selected-route Parent leaf + declared current-work frontier' });
|
|
372
|
+
if (state === 'grounded-to-discuss' && String(authority?.holderBinding?.state || 'unresolved') === 'unresolved') return Object.freeze({ kind: 'establish-session-holder-role-binding', target: authority?.role?.endpoint?.label || authority?.handoff?.to || '', basis: 'recipient Role qualification is separate from consuming-session holder binding; exact qualified selected-Handoff consumption may establish `handoff` assignment when authorized, otherwise an explicit authorized binding is required; no transport/provider/assistant-user identity inference is permitted' });
|
|
373
373
|
if (state === 'grounded-to-discuss' && String(authority?.holderBinding?.state || '') === 'qualified' && String(authority?.holderBinding?.authorization?.state || 'unresolved') !== 'qualified') return Object.freeze({
|
|
374
374
|
kind: 'resolve-session-holder-binding-authorization',
|
|
375
375
|
target: authority?.holderBinding?.authorization?.provenance?.roleArtifactPath || authority?.role?.endpoint?.label || authority?.handoff?.to || '',
|
|
376
|
-
basis: 'a matching
|
|
376
|
+
basis: 'a matching session Role binding is not semantic authorization by itself; exact qualified recipient Role Holder Relationship authority must establish the selected assignment mode'
|
|
377
377
|
});
|
|
378
378
|
if (state === 'grounded-to-discuss') return Object.freeze({ kind: topology.currentFrontier.length ? 'obtain-bounded-action-authority-or-human-gate' : 'resolve-current-work-frontier', target: topology.currentTasks[0]?.path || '', basis: 'discussion-ready but act-readiness condition is unresolved' });
|
|
379
379
|
if (continuity?.state === 'unproven') return Object.freeze({
|
|
@@ -129,10 +129,22 @@ function resolveDescriptorMaterial(bundle, descriptor, byteProvider, target = ''
|
|
|
129
129
|
const expectedRouteWorkspaceId = String(routeWorkspaceId || '');
|
|
130
130
|
const expectedRoutePath = normalizeWorkspacePath(routePath || '');
|
|
131
131
|
const matches = (descriptor.materialized || []).filter((entry) => {
|
|
132
|
-
|
|
132
|
+
const entryRequirementId = String(entry.requirementId || '');
|
|
133
|
+
const entrySourceRequirementId = String(entry.sourceRequirementId || '');
|
|
134
|
+
const entryRouteWorkspaceId = String(entry.routeWorkspaceId || '');
|
|
135
|
+
const entryRoutePath = normalizeWorkspacePath(entry.routePath || '');
|
|
136
|
+
const exactRequirementMatch = !expectedRequirementId || entryRequirementId === expectedRequirementId;
|
|
137
|
+
const routeScopedSourceMatch = Boolean(
|
|
138
|
+
expectedRequirementId &&
|
|
139
|
+
entrySourceRequirementId === expectedRequirementId &&
|
|
140
|
+
expectedRouteWorkspaceId && expectedRoutePath &&
|
|
141
|
+
entryRouteWorkspaceId === expectedRouteWorkspaceId &&
|
|
142
|
+
entryRoutePath === expectedRoutePath
|
|
143
|
+
);
|
|
144
|
+
if (expectedRequirementId && !exactRequirementMatch && !routeScopedSourceMatch) return false;
|
|
133
145
|
if (expectedTarget && String(entry.referenceTarget || '') !== expectedTarget) return false;
|
|
134
|
-
if (expectedRouteWorkspaceId &&
|
|
135
|
-
if (expectedRoutePath &&
|
|
146
|
+
if (expectedRouteWorkspaceId && entryRouteWorkspaceId && entryRouteWorkspaceId !== expectedRouteWorkspaceId) return false;
|
|
147
|
+
if (expectedRoutePath && entryRoutePath && entryRoutePath !== expectedRoutePath) return false;
|
|
136
148
|
return Boolean(expectedTarget || expectedRequirementId);
|
|
137
149
|
});
|
|
138
150
|
const byRepresentation = new Map();
|
|
@@ -247,82 +247,110 @@ function groundHolderBinding(input, handoff, role, findings) {
|
|
|
247
247
|
const recipientRoleKind = normalizeToken(handoff.toKind || (recipientRoleLabel ? 'role' : ''));
|
|
248
248
|
const roleRecipient = recipientRoleKind === 'role';
|
|
249
249
|
const explicitlySupplied = Boolean(roleLabel || holderId);
|
|
250
|
-
const sourceDetail = holderBindingSourceDetail(input, explicit, explicitlySupplied);
|
|
251
|
-
const assertionMode = String(explicit.assignmentMode || explicit.bindingMode || explicit.mechanism || input.holderAssignmentMode || input.sessionAssignmentMode || 'explicit-session').trim();
|
|
252
|
-
const authorization = projectHolderBindingAuthorization(role, { assertionMode });
|
|
253
250
|
const durableIdentity = holderDurableIdentityProjection(holderId);
|
|
254
251
|
|
|
255
|
-
if (!roleRecipient)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assertionMode
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
252
|
+
if (!roleRecipient) {
|
|
253
|
+
const assertionMode = String(explicit.assignmentMode || explicit.bindingMode || explicit.mechanism || input.holderAssignmentMode || input.sessionAssignmentMode || 'explicit-session').trim();
|
|
254
|
+
const sourceDetail = holderBindingSourceDetail(input, explicit, explicitlySupplied);
|
|
255
|
+
const authorization = projectHolderBindingAuthorization(role, { assertionMode });
|
|
256
|
+
return deepFreeze({
|
|
257
|
+
state: 'not-applicable', holderId, assertionMode, roleLabel, recipientRoleLabel,
|
|
258
|
+
recipientCompatibility: 'not-applicable', source: explicitlySupplied ? 'explicit-input' : 'none', sourceDetail,
|
|
259
|
+
authorization, durableIdentity, explicit: explicitlySupplied, inferredFromTransport: false,
|
|
260
|
+
boundary: 'The selected Handoff recipient is not a Role endpoint, so no consuming-session Role holder binding is required or inferred.'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!explicitlySupplied) {
|
|
265
|
+
const assertionMode = 'handoff';
|
|
266
|
+
const authorization = projectHolderBindingAuthorization(role, { assertionMode });
|
|
267
|
+
const exactHandoff = exactSelectedHandoffAssignmentEvidence(handoff, role);
|
|
268
|
+
const sourceDetail = handoffHolderBindingSourceDetail(handoff, role, exactHandoff, authorization);
|
|
269
|
+
if (exactHandoff && authorization.state === 'qualified') return deepFreeze({
|
|
270
|
+
state: 'qualified', holderId: '', assertionMode, roleLabel: recipientRoleLabel, recipientRoleLabel,
|
|
271
|
+
recipientCompatibility: 'matched', source: 'qualified-selected-handoff-consumption', sourceDetail,
|
|
272
|
+
authorization, durableIdentity, explicit: false, inferredFromTransport: false,
|
|
273
|
+
boundary: 'Qualified consumption of the exact selected Handoff binds this bounded Tooling session to its exact recipient Role only because that exact qualified Role authorizes the canonical `handoff` assignment mode. Package delivery, route position, provider/chat identity and Role inventory alone do not bind a holder; durable identity and broader semantic authority remain unestablished.'
|
|
274
|
+
});
|
|
275
|
+
return deepFreeze({
|
|
276
|
+
state: 'unresolved', holderId: '', assertionMode, roleLabel: '', recipientRoleLabel,
|
|
277
|
+
recipientCompatibility: 'unresolved', source: exactHandoff ? 'qualified-selected-handoff-consumption' : 'none', sourceDetail,
|
|
278
|
+
authorization, durableIdentity, explicit: false, inferredFromTransport: false,
|
|
279
|
+
boundary: 'No explicit holder declaration was supplied, and bounded Handoff assignment can qualify only from the exact selected qualified Handoff plus exact recipient Role material that directly authorizes the canonical `handoff` mode. Transport delivery, endpoint naming, cached Role inventory and assistant/user position are insufficient.'
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sourceDetail = holderBindingSourceDetail(input, explicit, true);
|
|
284
|
+
const assertionMode = String(explicit.assignmentMode || explicit.bindingMode || explicit.mechanism || input.holderAssignmentMode || input.sessionAssignmentMode || 'explicit-session').trim();
|
|
285
|
+
const authorization = projectHolderBindingAuthorization(role, { assertionMode });
|
|
270
286
|
|
|
271
287
|
if (!roleLabel) {
|
|
272
288
|
if (holderId) findings.push(portableFinding('warning', 'portable.cold-start.holder-binding.role-missing', 'A consuming-session holder identifier was supplied without an explicit Role capacity; the holder binding remains unresolved.', { holderId, recipientRole: recipientRoleLabel }));
|
|
273
289
|
return deepFreeze({
|
|
274
|
-
state: 'unresolved',
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
recipientRoleLabel,
|
|
279
|
-
recipientCompatibility: 'unresolved',
|
|
280
|
-
source: explicitlySupplied ? 'explicit-input' : 'none',
|
|
281
|
-
sourceDetail,
|
|
282
|
-
authorization,
|
|
283
|
-
durableIdentity,
|
|
284
|
-
explicit: explicitlySupplied,
|
|
285
|
-
inferredFromTransport: false,
|
|
286
|
-
boundary: 'Recipient Role and consuming-session holder are separate. No holder Role is inferred from route selection, transport identity, provider identity, assistant/user position, or participant declarations.'
|
|
290
|
+
state: 'unresolved', holderId, assertionMode, roleLabel: '', recipientRoleLabel,
|
|
291
|
+
recipientCompatibility: 'unresolved', source: 'explicit-input', sourceDetail, authorization, durableIdentity,
|
|
292
|
+
explicit: true, inferredFromTransport: false,
|
|
293
|
+
boundary: 'A partial explicit holder declaration is not completed from the Handoff path. Explicit-session and Handoff assignment remain mode-isolated.'
|
|
287
294
|
});
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
if (recipientRoleLabel && normalizeComparable(roleLabel) !== normalizeComparable(recipientRoleLabel)) {
|
|
291
298
|
findings.push(portableFinding('error', 'portable.cold-start.holder-binding.role-mismatch', 'Explicit consuming-session holder Role does not match the selected Handoff recipient Role.', { holderRole: roleLabel, recipientRole: recipientRoleLabel }));
|
|
292
299
|
return deepFreeze({
|
|
293
|
-
state: 'blocked',
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
recipientRoleLabel,
|
|
298
|
-
recipientCompatibility: 'mismatch',
|
|
299
|
-
source: 'explicit-input',
|
|
300
|
-
sourceDetail,
|
|
301
|
-
authorization,
|
|
302
|
-
durableIdentity,
|
|
303
|
-
explicit: true,
|
|
304
|
-
inferredFromTransport: false,
|
|
305
|
-
boundary: 'An explicit holder Role mismatch is contradictory and blocks act-ready grounding. Tooling does not relabel the session to make the route fit.'
|
|
300
|
+
state: 'blocked', holderId, assertionMode, roleLabel, recipientRoleLabel,
|
|
301
|
+
recipientCompatibility: 'mismatch', source: 'explicit-input', sourceDetail, authorization, durableIdentity,
|
|
302
|
+
explicit: true, inferredFromTransport: false,
|
|
303
|
+
boundary: 'An explicit holder Role mismatch is contradictory and blocks act-ready grounding. Tooling does not relabel the session or fall back to Handoff assignment to make the route fit.'
|
|
306
304
|
});
|
|
307
305
|
}
|
|
308
306
|
|
|
309
307
|
return deepFreeze({
|
|
310
|
-
state: 'qualified',
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
roleLabel,
|
|
314
|
-
recipientRoleLabel,
|
|
315
|
-
recipientCompatibility: 'matched',
|
|
316
|
-
source: 'explicit-input',
|
|
317
|
-
sourceDetail,
|
|
318
|
-
authorization,
|
|
319
|
-
durableIdentity,
|
|
320
|
-
explicit: true,
|
|
321
|
-
inferredFromTransport: false,
|
|
308
|
+
state: 'qualified', holderId, assertionMode, roleLabel, recipientRoleLabel,
|
|
309
|
+
recipientCompatibility: 'matched', source: 'explicit-input', sourceDetail, authorization, durableIdentity,
|
|
310
|
+
explicit: true, inferredFromTransport: false,
|
|
322
311
|
boundary: 'Explicit consuming-session Role-capacity binding only. This binds the current Tooling invocation/session to the selected recipient Role capacity; it does not prove a human identity, consent, or authority beyond the qualified Handoff/Role/Task boundaries.'
|
|
323
312
|
});
|
|
324
313
|
}
|
|
325
314
|
|
|
315
|
+
function exactSelectedHandoffAssignmentEvidence(handoff = {}, role = {}) {
|
|
316
|
+
const artifact = role?.material?.artifact || {};
|
|
317
|
+
return String(handoff.schemaId || '') === 'tiinex.handoff.v1'
|
|
318
|
+
&& Boolean(String(handoff.routeId || '').trim())
|
|
319
|
+
&& Boolean(String(handoff.routePointerPath || '').trim())
|
|
320
|
+
&& Boolean(String(handoff.workspaceRelativePath || '').trim())
|
|
321
|
+
&& /^[0-9a-f]{64}$/i.test(String(handoff.sha256 || ''))
|
|
322
|
+
&& String(role?.state || '') === 'qualified'
|
|
323
|
+
&& String(role?.material?.state || '') === 'qualified'
|
|
324
|
+
&& Boolean(String(artifact.path || '').trim())
|
|
325
|
+
&& /^[0-9a-f]{64}$/i.test(String(artifact.sha256 || ''));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function handoffHolderBindingSourceDetail(handoff = {}, role = {}, exactHandoff = false, authorization = {}) {
|
|
329
|
+
const artifact = role?.material?.artifact || {};
|
|
330
|
+
const qualified = exactHandoff && String(authorization?.state || '') === 'qualified';
|
|
331
|
+
return deepFreeze({
|
|
332
|
+
kind: exactHandoff ? 'qualified-selected-handoff-consumption' : 'none',
|
|
333
|
+
locator: exactHandoff ? String(handoff.routePointerPath || handoff.workspaceRelativePath || '') : '',
|
|
334
|
+
authorityClass: exactHandoff ? 'selected-handoff-assignment-evidence' : 'none',
|
|
335
|
+
semanticAuthorityState: qualified ? 'qualified-bounded-handoff-assignment' : 'not-established',
|
|
336
|
+
qualifiedMaterialSource: Boolean(exactHandoff),
|
|
337
|
+
assignmentMode: 'handoff',
|
|
338
|
+
routeId: String(handoff.routeId || ''),
|
|
339
|
+
routePointerPath: String(handoff.routePointerPath || ''),
|
|
340
|
+
handoffArtifactPath: String(handoff.workspaceRelativePath || ''),
|
|
341
|
+
handoffArtifactSha256: String(handoff.sha256 || ''),
|
|
342
|
+
roleArtifactPath: String(artifact.path || ''),
|
|
343
|
+
roleArtifactSha256: String(artifact.sha256 || ''),
|
|
344
|
+
roleSchemaId: String(artifact.schemaId || ''),
|
|
345
|
+
roleLabel: String(artifact.roleLabel || role?.endpoint?.label || ''),
|
|
346
|
+
authorizationState: String(authorization?.state || 'unresolved'),
|
|
347
|
+
authorizationBasis: String(authorization?.provenance?.basis || ''),
|
|
348
|
+
boundary: exactHandoff
|
|
349
|
+
? 'Exact selected qualified Handoff consumption plus exact qualified recipient Role material are the bounded assignment evidence. This is semantic Handoff/Role evidence, not package-delivery, provider, model, chat-position, filename or neighboring-route inference.'
|
|
350
|
+
: 'Exact selected Handoff/Role assignment evidence is not established; no Handoff-mode holder binding is projected.'
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
326
354
|
function holderDurableIdentityProjection(holderId = '') {
|
|
327
355
|
return deepFreeze({
|
|
328
356
|
state: 'not-established',
|
|
@@ -390,6 +390,7 @@ export function parseHandoffGrounding(markdown, route) {
|
|
|
390
390
|
returnTo: sectionField(completion, 'Return To')
|
|
391
391
|
}),
|
|
392
392
|
routeId: String(route?.id || ''),
|
|
393
|
+
routePointerPath: String(route?.pointerPath || ''),
|
|
393
394
|
workspaceId: String(route?.workspaceId || ''),
|
|
394
395
|
workspaceRelativePath: String(route?.workspaceRelativeHandoffPath || route?.workspaceRelativePath || ''),
|
|
395
396
|
packagePath: String(route?.packagePath || ''),
|
|
@@ -405,7 +406,7 @@ function markdownReferenceTarget(value = '') {
|
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
export function emptyHandoffGrounding() {
|
|
408
|
-
return deepFreeze({ schemaId: '', purpose: '', from: '', fromKind: '', fromReference: '', to: '', toKind: '', toReference: '', transfers: Object.freeze([]), retainedResponsibilities: Object.freeze([]), completionExpectation: Object.freeze({ signalKind: '', signalMeaning: '', returnTo: '' }), routeId: '', workspaceId: '', workspaceRelativePath: '', packagePath: '', sha256: '', boundary: 'No Handoff material supplied.' });
|
|
409
|
+
return deepFreeze({ schemaId: '', purpose: '', from: '', fromKind: '', fromReference: '', to: '', toKind: '', toReference: '', transfers: Object.freeze([]), retainedResponsibilities: Object.freeze([]), completionExpectation: Object.freeze({ signalKind: '', signalMeaning: '', returnTo: '' }), routeId: '', routePointerPath: '', workspaceId: '', workspaceRelativePath: '', packagePath: '', sha256: '', boundary: 'No Handoff material supplied.' });
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
export function resolveGroundingRouteMarkdown(bundle = {}, selectedRoute = {}, findings = [], context = null) {
|
|
@@ -8,7 +8,7 @@ export function buildEndpointRolePointerChain(input = {}) {
|
|
|
8
8
|
const findings = [];
|
|
9
9
|
let lineageParent = input.lineageParent || null;
|
|
10
10
|
let nextDimension = String(input.nextDimension || '');
|
|
11
|
-
for (const requirement of input.requirements || []) {
|
|
11
|
+
for (const requirement of orderedEndpointRequirements(input.requirements || [])) {
|
|
12
12
|
const target = input.resolveRoleMaterialTarget(requirement, input.descriptor, input.workspaceById, input.cache, input.route);
|
|
13
13
|
if (target.state !== 'qualified') {
|
|
14
14
|
findings.push(finding('error', `portable.handoff-v2-surface.endpoint-role.${target.reason || 'unresolved'}`, 'Endpoint Role requirement did not resolve to one exact carried Workspace/cache representation.', { routeId: String(input.route?.id || ''), requirementId: String(requirement.id || '') }));
|
|
@@ -175,5 +175,18 @@ export function buildParticipantRolePointerChain(input = {}) {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function orderedEndpointRequirements(requirements = []) {
|
|
179
|
+
return [...requirements].map((item, index) => ({ item, index })).sort((a, b) => {
|
|
180
|
+
const rank = (entry) => {
|
|
181
|
+
const party = String(entry?.party || entry?.fields?.Side || '').trim().toLowerCase();
|
|
182
|
+
if (party === 'from') return 0;
|
|
183
|
+
if (party === 'to') return 1;
|
|
184
|
+
return 2;
|
|
185
|
+
};
|
|
186
|
+
const delta = rank(a.item) - rank(b.item);
|
|
187
|
+
return delta || a.index - b.index;
|
|
188
|
+
}).map(({ item }) => item);
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function safeToken(value = '') { return String(value || '').trim().toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'workspace'; }
|
|
179
192
|
function finding(severity, code, message, extra = {}) { return Object.freeze({ severity, code, message, ...extra }); }
|
|
@@ -5,7 +5,7 @@ export function projectRecipientV2Routes(routePointers = [], endpointPointers =
|
|
|
5
5
|
const qualified = (qualifiedRoutes || []).find((route) => String(route.workspaceId || '') === String(item.facts?.workspaceId || '') && String(route.workspaceRelativePath || '') === String(item.facts?.workspaceRelativeHandoffPath || '')) || null;
|
|
6
6
|
return Object.freeze({
|
|
7
7
|
pointerPath: item.path,
|
|
8
|
-
routeId: String(item.facts?.routeId ||
|
|
8
|
+
routeId: String(item.facts?.routeId || ''),
|
|
9
9
|
workspaceId: String(item.facts?.workspaceId || ''),
|
|
10
10
|
workspaceRelativeHandoffPath: String(item.facts?.workspaceRelativeHandoffPath || ''),
|
|
11
11
|
returnCarrierReservation: item.facts?.returnCarrierReservation || qualified?.returnCarrierReservation || null,
|
|
@@ -163,10 +163,10 @@ function buildRecipientFacingV2PackageV1Prepared(input = {}, sealedByWorkspaceId
|
|
|
163
163
|
const binding = bindingForWorkspace(descriptor, owningWorkspace.workspaceId);
|
|
164
164
|
let lineageParent = cache?.parent || owningWorkspace.parent;
|
|
165
165
|
let nextDimension = cache ? `${plan.prefix}-1-1` : `${plan.prefix}-1`;
|
|
166
|
-
const endpointChain = buildEndpointRolePointerChain({ requirements: route.materialRequirements?.endpointRoles || [], descriptor, workspaceById, cache, workspace: owningWorkspace, route, createdAt, lineageParent, nextDimension, resolveRoleMaterialTarget: roleMaterialTarget, parentAuthority: recipientV2ParentAuthority });
|
|
167
|
-
files.push(...endpointChain.files); topology.endpointRoles.push(...endpointChain.roles); findings.push(...endpointChain.findings); lineageParent = endpointChain.lineageParent; nextDimension = endpointChain.nextDimension;
|
|
168
166
|
const participantChain = buildParticipantRolePointerChain({ requirements: route.materialRequirements?.participantRoles || [], descriptor, workspaceById, cache, workspace: owningWorkspace, route, createdAt, lineageParent, nextDimension, resolveRoleMaterialTarget: roleMaterialTarget, parentAuthority: recipientV2ParentAuthority });
|
|
169
167
|
files.push(...participantChain.files); topology.participantRoles.push(...participantChain.roles); findings.push(...participantChain.findings); lineageParent = participantChain.lineageParent; nextDimension = participantChain.nextDimension;
|
|
168
|
+
const endpointChain = buildEndpointRolePointerChain({ requirements: route.materialRequirements?.endpointRoles || [], descriptor, workspaceById, cache, workspace: owningWorkspace, route, createdAt, lineageParent, nextDimension, resolveRoleMaterialTarget: roleMaterialTarget, parentAuthority: recipientV2ParentAuthority });
|
|
169
|
+
files.push(...endpointChain.files); topology.endpointRoles.push(...endpointChain.roles); findings.push(...endpointChain.findings); lineageParent = endpointChain.lineageParent; nextDimension = endpointChain.nextDimension;
|
|
170
170
|
const pointerPath = `${nextDimension}-handoff-pointer.trace.md`;
|
|
171
171
|
const routeEntry = (binding?.entryMap?.entries || []).find((entry) => String(entry.path || '') === String(route.workspaceRelativePath || ''));
|
|
172
172
|
const pointerFacts = {
|
|
@@ -183,8 +183,8 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
|
|
|
183
183
|
const routeDimension = cache ? `001-${plan.workspace.ordinal}-1-${plan.ordinal}` : `001-${plan.workspace.ordinal}-${plan.ordinal}`;
|
|
184
184
|
let lineageParent = cache?.parent || workspace.parent;
|
|
185
185
|
let nextDimension = routeDimension;
|
|
186
|
-
const
|
|
187
|
-
requirements: route.materialRequirements?.
|
|
186
|
+
const participantChain = buildParticipantRolePointerChain({
|
|
187
|
+
requirements: route.materialRequirements?.participantRoles || [],
|
|
188
188
|
descriptor,
|
|
189
189
|
workspaceById,
|
|
190
190
|
cache,
|
|
@@ -196,13 +196,13 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
|
|
|
196
196
|
resolveRoleMaterialTarget: roleMaterialTarget,
|
|
197
197
|
parentAuthority: recipientV2ParentAuthority
|
|
198
198
|
});
|
|
199
|
-
files.push(...
|
|
200
|
-
topology.
|
|
201
|
-
findings.push(...
|
|
202
|
-
lineageParent =
|
|
203
|
-
nextDimension =
|
|
204
|
-
const
|
|
205
|
-
requirements: route.materialRequirements?.
|
|
199
|
+
files.push(...participantChain.files);
|
|
200
|
+
topology.participantRoles.push(...participantChain.roles);
|
|
201
|
+
findings.push(...participantChain.findings);
|
|
202
|
+
lineageParent = participantChain.lineageParent;
|
|
203
|
+
nextDimension = participantChain.nextDimension;
|
|
204
|
+
const endpointChain = buildEndpointRolePointerChain({
|
|
205
|
+
requirements: route.materialRequirements?.endpointRoles || [],
|
|
206
206
|
descriptor,
|
|
207
207
|
workspaceById,
|
|
208
208
|
cache,
|
|
@@ -214,11 +214,11 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
|
|
|
214
214
|
resolveRoleMaterialTarget: roleMaterialTarget,
|
|
215
215
|
parentAuthority: recipientV2ParentAuthority
|
|
216
216
|
});
|
|
217
|
-
files.push(...
|
|
218
|
-
topology.
|
|
219
|
-
findings.push(...
|
|
220
|
-
lineageParent =
|
|
221
|
-
nextDimension =
|
|
217
|
+
files.push(...endpointChain.files);
|
|
218
|
+
topology.endpointRoles.push(...endpointChain.roles);
|
|
219
|
+
findings.push(...endpointChain.findings);
|
|
220
|
+
lineageParent = endpointChain.lineageParent;
|
|
221
|
+
nextDimension = endpointChain.nextDimension;
|
|
222
222
|
const pointerPath = `${nextDimension}-handoff-pointer.trace.md`;
|
|
223
223
|
const pointerFacts = {
|
|
224
224
|
workspaceId: workspace.workspaceId,
|
|
@@ -4,7 +4,8 @@ import { normalizePortableInput } from '../input/portable.input.js';
|
|
|
4
4
|
import { portableFinding, summarizePortableFindings } from '../findings.js';
|
|
5
5
|
import { planPortableArtifact } from '../schema/schema.guide.js';
|
|
6
6
|
import { indexPortableLoadedParentRecords, projectPortableLoadedParentRecord, resolvePortableLoadedParentReference } from './loaded.parent.js';
|
|
7
|
-
import { allocateContinuationPath, allocateRootArtifactPath } from '../../../transitions/record.transitions.js';
|
|
7
|
+
import { allocateContinuationPath, allocateDirectoryArtifactPath, allocateRootArtifactPath } from '../../../transitions/record.transitions.js';
|
|
8
|
+
import { normalizePortableParentRecord, qualifyPortableExactParent } from '../draft/draft.exact.js';
|
|
8
9
|
|
|
9
10
|
export const PORTABLE_EPISTEMIC_PLAN_SCHEMA_ID = 'tiinex.portable.epistemic-materialization-plan.v1';
|
|
10
11
|
|
|
@@ -69,18 +70,20 @@ function planProposal({ proposal, index, proposals, proposalIds, loadedIndex, ma
|
|
|
69
70
|
const clarificationNeeds = [];
|
|
70
71
|
const contract = buildArtifactCreationContract({ schemaId: proposal.schemaId, transitionType: proposal.parentRef ? 'continue-from-record' : 'create-artifact' });
|
|
71
72
|
const artifactPlan = planPortableArtifact({ ...material, schemaId: proposal.schemaId, task: proposal.parentRef ? 'continue' : 'create', values: proposal.values || {}, inputs: proposal.values || {} }, options);
|
|
72
|
-
const parent = resolveParentReference(proposal.parentRef, { loadedIndex, proposalIds, proposalIndex: index, proposals });
|
|
73
|
+
const parent = resolveParentReference(proposal.parentRef, { loadedIndex, proposalIds, proposalIndex: index, proposals, suppliedParentRecord: proposal.parentRecord });
|
|
73
74
|
const parentForAllocation = parent.status === 'resolved'
|
|
74
75
|
? parent.kind === 'proposal'
|
|
75
76
|
? plannedParentRecord(plannedById.get(parent.parent?.proposalId || ''))
|
|
76
77
|
: parent.parent
|
|
77
78
|
: null;
|
|
78
|
-
const allocationOptions = Object.freeze({ existingPaths: Object.freeze([...occupiedPaths]), path: proposal.path || '' });
|
|
79
|
+
const allocationOptions = Object.freeze({ existingPaths: Object.freeze([...occupiedPaths]), path: proposal.path || '', targetDirectory: proposal.targetDirectory || '' });
|
|
79
80
|
const allocated = proposal.parentRef && parentForAllocation
|
|
80
81
|
? allocateContinuationPath({ parentRecord: parentForAllocation, targetId: proposal.schemaId, targetLabel: proposal.schemaId, title: proposal.title || proposal.summary || proposal.id }, allocationOptions)
|
|
81
|
-
: !proposal.parentRef
|
|
82
|
-
?
|
|
83
|
-
:
|
|
82
|
+
: !proposal.parentRef && proposal.targetDirectory
|
|
83
|
+
? allocateDirectoryArtifactPath({ targetDirectory: proposal.targetDirectory, targetId: proposal.schemaId, targetLabel: proposal.schemaId, title: proposal.title || proposal.summary || proposal.id }, allocationOptions)
|
|
84
|
+
: !proposal.parentRef
|
|
85
|
+
? allocateRootArtifactPath({ targetId: proposal.schemaId, targetLabel: proposal.schemaId, title: proposal.title || proposal.summary || proposal.id }, allocationOptions)
|
|
86
|
+
: Object.freeze({ path: proposal.path || '', policy: Object.freeze({}) });
|
|
84
87
|
|
|
85
88
|
if (!proposal.schemaId) {
|
|
86
89
|
findings.push(portableFinding('error', 'portable.materialization.schema.required', 'Each proposal must declare an implemented schema id; schema meaning is never inferred by the writer.', { proposalId: proposal.id }));
|
|
@@ -130,6 +133,7 @@ function planProposal({ proposal, index, proposals, proposalIds, loadedIndex, ma
|
|
|
130
133
|
rationale: proposal.rationale,
|
|
131
134
|
evidenceRefs: proposal.evidenceRefs,
|
|
132
135
|
parentRef: proposal.parentRef,
|
|
136
|
+
targetDirectory: proposal.targetDirectory,
|
|
133
137
|
parent: parent.status === 'resolved' ? parent.parent : null,
|
|
134
138
|
parentKind: parent.status === 'resolved' ? parent.kind : '',
|
|
135
139
|
creationContract: contract,
|
|
@@ -148,6 +152,8 @@ function normalizeProposals(value) {
|
|
|
148
152
|
mode: clean(raw.mode || ''),
|
|
149
153
|
schemaId: clean(raw.schemaId || raw.schema || ''),
|
|
150
154
|
parentRef: exact(raw.parentRef ?? raw.parent ?? ''),
|
|
155
|
+
parentRecord: raw.parentRecord && typeof raw.parentRecord === 'object' ? Object.freeze(clone(raw.parentRecord)) : null,
|
|
156
|
+
targetDirectory: clean(raw.targetDirectory || raw.directory || ''),
|
|
151
157
|
path: clean(raw.path || ''),
|
|
152
158
|
title: clean(raw.title || ''),
|
|
153
159
|
summary: clean(raw.summary || ''),
|
|
@@ -168,9 +174,17 @@ function plannedParentRecord(entry = null) {
|
|
|
168
174
|
return Object.freeze({ id: entry.id || '', path: entry.path, schemaId: entry.schemaId || '', title: entry.title || entry.summary || entry.id || '' });
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
function resolveParentReference(ref, { loadedIndex, proposalIds, proposalIndex, proposals }) {
|
|
177
|
+
function resolveParentReference(ref, { loadedIndex, proposalIds, proposalIndex, proposals, suppliedParentRecord = null }) {
|
|
172
178
|
if (!ref) return { status: 'none', kind: 'none', parent: null };
|
|
173
179
|
const reference = exact(ref);
|
|
180
|
+
if (suppliedParentRecord && typeof suppliedParentRecord === 'object') {
|
|
181
|
+
const normalized = normalizePortableParentRecord(suppliedParentRecord);
|
|
182
|
+
const declared = new Set([exact(normalized.id), exact(normalized.path)].filter(Boolean));
|
|
183
|
+
if (!declared.has(reference)) return { status: 'mismatch', code: 'portable.materialization.parent.supplied-reference-mismatch', message: 'The supplied Parent record does not identify the declared Parent reference.', candidates: [...declared] };
|
|
184
|
+
const qualification = qualifyPortableExactParent(normalized, 'continue-from-record');
|
|
185
|
+
if (!['qualified', 'qualified-local-continuity'].includes(String(qualification.state || ''))) return { status: 'unqualified', code: `portable.materialization.parent.supplied-${qualification.reason || 'unqualified'}`, message: 'The supplied Parent record is not qualified for continuation authoring.', candidates: [] };
|
|
186
|
+
return { status: 'resolved', kind: 'supplied-record', parent: qualification.snapshot };
|
|
187
|
+
}
|
|
174
188
|
const proposalRef = reference.startsWith('proposal:') ? reference.slice('proposal:'.length) : proposalIds.has(reference) ? reference : '';
|
|
175
189
|
if (proposalRef) {
|
|
176
190
|
const targetIndex = proposals.findIndex((proposal) => proposal.id === proposalRef);
|
|
@@ -169,19 +169,21 @@ export function allocateContinuationPath({ parentRecord = {}, targetId = '', tar
|
|
|
169
169
|
const explicitPath = canonicalLocalPath(options.path || options.draftPath || '');
|
|
170
170
|
if (explicitPath) return { path: uniqueTransitionPath(explicitPath, occupied), policy: pathPolicyForExplicit(explicitPath) };
|
|
171
171
|
const parentPath = externalWebArtifactUrl(parentRecord) ? '' : canonicalLocalPath(parentRecord.path || parentRecord.sourcePath || parentRecord.sourceTarget?.sourceArtifactPath || '');
|
|
172
|
-
const parentDir = parentDirectory(parentPath)
|
|
173
|
-
const
|
|
174
|
-
const
|
|
172
|
+
const parentDir = parentDirectory(parentPath);
|
|
173
|
+
const requestedRaw = String(options.targetDirectory || options.directory || '').trim();
|
|
174
|
+
const requestedExplicit = Boolean(requestedRaw);
|
|
175
|
+
const requestedDir = requestedRaw === '.' ? '' : canonicalLocalPath(requestedRaw);
|
|
176
|
+
const targetDir = requestedExplicit ? requestedDir : (parentDir || '.topics');
|
|
175
177
|
const parentPrefix = lineagePrefixFromPath(parentPath);
|
|
176
178
|
const labelSlug = slugify(title || parentRecord.title || targetLabel || 'continuation');
|
|
177
179
|
const targetSlug = slugify(targetLabel || labelFromSchemaId(targetId) || 'leaf');
|
|
178
180
|
const extension = '.trace.md';
|
|
179
|
-
const directoryLocal = Boolean(
|
|
181
|
+
const directoryLocal = Boolean(requestedExplicit && requestedDir !== parentDir);
|
|
180
182
|
const policy = {
|
|
181
183
|
schema: 'tiinex.transition.path-policy.v1',
|
|
182
184
|
kind: directoryLocal ? 'directory-local-continuation' : 'same-parent-directory',
|
|
183
185
|
parentDirectory: parentDir,
|
|
184
|
-
targetDirectory: targetDir,
|
|
186
|
+
targetDirectory: requestedExplicit && requestedDir === '' ? '.' : targetDir,
|
|
185
187
|
parentPath,
|
|
186
188
|
parentLineagePrefix: parentPrefix,
|
|
187
189
|
labelSlug,
|
|
@@ -196,14 +198,16 @@ export function allocateDirectoryArtifactPath({ targetDirectory = '.topics', tar
|
|
|
196
198
|
const occupied = existingTransitionPaths(options);
|
|
197
199
|
const explicitPath = canonicalLocalPath(options.path || options.draftPath || '');
|
|
198
200
|
if (explicitPath) return { path: uniqueTransitionPath(explicitPath, occupied), policy: pathPolicyForExplicit(explicitPath) };
|
|
199
|
-
const
|
|
201
|
+
const targetRaw = String(targetDirectory ?? '').trim();
|
|
202
|
+
const explicitRoot = targetRaw === '.';
|
|
203
|
+
const dir = explicitRoot ? '' : (canonicalLocalPath(targetRaw || '.topics') || '.topics');
|
|
200
204
|
const labelSlug = slugify(title || targetLabel || labelFromSchemaId(targetId) || 'artifact');
|
|
201
205
|
const targetSlug = slugify(targetLabel || labelFromSchemaId(targetId) || 'artifact');
|
|
202
206
|
const extension = '.trace.md';
|
|
203
207
|
const policy = {
|
|
204
208
|
schema: 'tiinex.transition.path-policy.v1',
|
|
205
209
|
kind: 'directory-local-root',
|
|
206
|
-
targetDirectory: dir,
|
|
210
|
+
targetDirectory: explicitRoot ? '.' : dir,
|
|
207
211
|
labelSlug,
|
|
208
212
|
targetSlug,
|
|
209
213
|
extension,
|
|
@@ -214,20 +218,25 @@ export function allocateDirectoryArtifactPath({ targetDirectory = '.topics', tar
|
|
|
214
218
|
|
|
215
219
|
function pathFromPolicy(policy = {}, occupied = new Set()) {
|
|
216
220
|
const kind = String(policy.kind || '').trim();
|
|
217
|
-
const
|
|
221
|
+
const hasTargetDirectory = Object.prototype.hasOwnProperty.call(policy, 'targetDirectory');
|
|
222
|
+
const hasParentDirectory = Object.prototype.hasOwnProperty.call(policy, 'parentDirectory');
|
|
223
|
+
const rawDirectory = hasTargetDirectory ? String(policy.targetDirectory ?? '') : hasParentDirectory ? String(policy.parentDirectory ?? '') : '.topics';
|
|
224
|
+
const rootDirectory = rawDirectory === '.' || ((hasTargetDirectory || hasParentDirectory) && rawDirectory === '');
|
|
225
|
+
const dir = rootDirectory ? '' : (canonicalLocalPath(rawDirectory || '.topics') || '.topics');
|
|
226
|
+
const pathPrefix = dir ? `${dir}/` : '';
|
|
218
227
|
const extension = String(policy.extension || '.trace.md').startsWith('.') ? String(policy.extension || '.trace.md') : `.${policy.extension}`;
|
|
219
228
|
const labelSlug = slugify(policy.labelSlug || 'continuation');
|
|
220
229
|
const targetSlug = slugify(policy.targetSlug || 'leaf');
|
|
221
230
|
const parentPrefix = String(policy.parentLineagePrefix || '').trim();
|
|
222
231
|
if (kind === 'directory-local-root' || kind === 'directory-local-continuation') {
|
|
223
232
|
const rootPrefix = nextDirectoryRootLineagePrefix(dir, occupied);
|
|
224
|
-
return `${
|
|
233
|
+
return `${pathPrefix}${rootPrefix}-${labelSlug}.${extension.replace(/^\./, '')}`;
|
|
225
234
|
}
|
|
226
235
|
if (parentPrefix) {
|
|
227
236
|
const childPrefix = nextChildLineagePrefix(parentPrefix, dir, occupied);
|
|
228
|
-
return `${
|
|
237
|
+
return `${pathPrefix}${childPrefix}-${labelSlug}.${extension.replace(/^\./, '')}`;
|
|
229
238
|
}
|
|
230
|
-
return uniqueTransitionPath(`${
|
|
239
|
+
return uniqueTransitionPath(`${pathPrefix}${labelSlug}--${targetSlug}${extension}`, occupied);
|
|
231
240
|
}
|
|
232
241
|
|
|
233
242
|
function pathPolicyForExplicit(path = '') {
|