@tiinex/core 0.1.1 → 0.3.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/README.md +5 -5
- package/package.json +5 -5
- package/src/tooling/portable/adapters/cli/cli.common-author.js +68 -11
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +3 -1
- package/src/tooling/portable/adapters/cli/cli.help.js +4 -4
- package/src/tooling/portable/adapters/cli/cli.material-policy.js +1 -0
- package/src/tooling/portable/adapters/cli/cli.operator-bridge.js +13 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +3 -1
- package/src/tooling/portable/adapters/node/handoff.manufacture.packageParent.js +32 -3
- package/src/tooling/portable/handoff/sourceFrontierComparison.js +186 -0
- package/src/tooling/portable/operation.catalog.package.js +8 -0
- package/src/transitions/record.transitions.js +52 -3
- package/src/tooling/portable/bootstrap/bootstrap.case.mjs +0 -43
package/README.md
CHANGED
|
@@ -31,15 +31,15 @@ Source paths intentionally follow the same `src/...` conventions used by other T
|
|
|
31
31
|
repositories where the concepts match. npm distribution preserves this source layout
|
|
32
32
|
rather than inventing a second directory vocabulary.
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## Current implementation frontier
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
`@tiinex/core` is a published package boundary; package publication does not by itself establish browser/product acceptance or semantic authority. App/Site/Verse integration remains separately qualified by those consumers.
|
|
37
37
|
|
|
38
|
-
The explicit package export map
|
|
38
|
+
The explicit package export map exposes the shared module paths used by App. These preserve the mirrored layout, not a second implementation. `tools/tiinex-portable.mjs` remains a transitional compatibility entrypoint while dedicated CLI/Interop hosts are stabilized. See [`docs/ARCHITECTURE-BOUNDARIES.md`](docs/ARCHITECTURE-BOUNDARIES.md) for the carrier-bootstrap, schema-material and external-Interop split.
|
|
39
39
|
|
|
40
40
|
Application data is a declared-data projection: parsing and exact reference resolution are not schema validation, integrity verification or authority qualification. Missing Parent evidence remains unresolved. Companion byte access belongs to registered host readers, not automatic filesystem or network permissions.
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## npm release frontier
|
|
44
44
|
|
|
45
|
-
Master-only automatic versioning/publication is implemented through the
|
|
45
|
+
Master-only automatic versioning/publication is implemented through the Core Node release helper. See `docs/NPM-PUBLISH.md`. OIDC/Trusted Publisher configuration is an external repository/package setting and remains separate from source qualification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,
|
|
@@ -165,12 +165,12 @@
|
|
|
165
165
|
"type": "git",
|
|
166
166
|
"url": "git+https://github.com/Tiinex/core.git"
|
|
167
167
|
},
|
|
168
|
-
"gitHead": "
|
|
168
|
+
"gitHead": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
|
|
169
169
|
"tiinexRelease": {
|
|
170
170
|
"policy": "tiinex.master-npm-release.v1",
|
|
171
|
-
"sourceCommit": "
|
|
172
|
-
"sourceTree": "
|
|
171
|
+
"sourceCommit": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
|
|
172
|
+
"sourceTree": "cbf5f25e875efac11c017d9914efa673bdfc2edb",
|
|
173
173
|
"repository": "Tiinex/core",
|
|
174
|
-
"previousVersion":
|
|
174
|
+
"previousVersion": "0.2.0"
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseArtifactMarkdown } from '../../../../artifacts/artifact.parse.js';
|
|
4
4
|
import { buildArtifactCreationContract } from '../../../../schemas/creation.contracts.js';
|
|
@@ -8,6 +8,7 @@ import { sha256Hex, utf8Bytes } from '../../../../export/package.bytes.js';
|
|
|
8
8
|
import { resolveSchemaModule } from '../../../../schemas/resolver.js';
|
|
9
9
|
import { loadNodePortableInput } from '../../input/node.input.js';
|
|
10
10
|
import { runPortableOperation } from '../../operation.catalog.js';
|
|
11
|
+
import { allocateContinuationPath, allocateDirectoryArtifactPath } from '../../../../transitions/record.transitions.js';
|
|
11
12
|
|
|
12
13
|
const STATE_RELATIVE_PATH = '.tiinex/continuation.json';
|
|
13
14
|
|
|
@@ -16,21 +17,27 @@ export async function runCommonAuthorCli(parsed = {}, runtime = {}) {
|
|
|
16
17
|
const workspaceRoot = path.resolve(String(flags.workspace || parsed.positionals?.[0] || '.'));
|
|
17
18
|
const state = await readContinuationState(workspaceRoot);
|
|
18
19
|
const schemaId = String(flags.schema || '').trim();
|
|
19
|
-
const
|
|
20
|
+
const requestedArtifactRelativePath = normalizeWorkspaceRelativePath(flags.path || parsed.positionals?.[1] || '');
|
|
21
|
+
const targetDirectory = normalizeWorkspaceRelativePath(flags.directory || flags.dir || '');
|
|
20
22
|
const bodyPath = String(flags.body || flags.content || '').trim();
|
|
21
23
|
if (!schemaId) throw new Error('portable.cli.author.schema.required');
|
|
22
|
-
if (!
|
|
24
|
+
if (!requestedArtifactRelativePath && !targetDirectory) throw new Error('portable.cli.author.path-or-directory.required');
|
|
23
25
|
if (!bodyPath) throw new Error('portable.cli.author.body.required');
|
|
24
|
-
const artifactPath = safeWorkspaceTarget(workspaceRoot, artifactRelativePath);
|
|
25
26
|
const bodyMarkdown = (await readFile(path.resolve(bodyPath), 'utf8')).trim();
|
|
26
27
|
if (!bodyMarkdown) throw new Error('portable.cli.author.body.empty');
|
|
28
|
+
const title = String(flags.title || firstHeading(bodyMarkdown) || state?.roleLabel || schemaId).trim();
|
|
27
29
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const parentReference = resolveParentReference(flags, state);
|
|
31
|
+
const parentSource = String(flags['parent-source'] || flags['parent-file'] || '').trim();
|
|
32
|
+
if (isWorkspaceQualifiedReference(parentReference) && !parentSource) throw new Error('portable.cli.author.parent-source.required');
|
|
33
|
+
if (parentSource && !parentReference) throw new Error('portable.cli.author.parent.required');
|
|
34
|
+
if (isWorkspaceQualifiedReference(parentReference) && !requestedArtifactRelativePath && !targetDirectory) throw new Error('portable.cli.author.cross-workspace-parent.target-required');
|
|
35
|
+
const parentPath = parentReference ? (parentSource ? path.resolve(parentSource) : safeWorkspaceTarget(workspaceRoot, parentReference)) : '';
|
|
36
|
+
const artifactRelativePath = requestedArtifactRelativePath || await allocateArtifactRelativePath({ workspaceRoot, targetDirectory, parentRelativePath: parentReference, schemaId, title });
|
|
37
|
+
const artifactPath = safeWorkspaceTarget(workspaceRoot, artifactRelativePath);
|
|
38
|
+
const parentRecord = parentPath ? await parentRecordFromArtifact(parentPath, parentReference, { workspaceRoot, childRelativePath: artifactRelativePath }) : {};
|
|
31
39
|
const transitionType = String(flags.transition || defaultTransition(schemaId, Boolean(parentPath))).trim();
|
|
32
40
|
const contract = buildArtifactCreationContract({ schemaId, transitionType });
|
|
33
|
-
const title = String(flags.title || firstHeading(bodyMarkdown) || state?.roleLabel || schemaId).trim();
|
|
34
41
|
const summary = String(flags.summary || title).trim();
|
|
35
42
|
const authors = String(flags.authors || state?.roleLabel || '').trim();
|
|
36
43
|
const why = Object.prototype.hasOwnProperty.call(flags, 'why') ? String(flags.why || '').trim() : '';
|
|
@@ -94,7 +101,7 @@ export async function runCommonAuthorCli(parsed = {}, runtime = {}) {
|
|
|
94
101
|
schema: 'tiinex.portable.common-author.result.v1',
|
|
95
102
|
operation: 'author',
|
|
96
103
|
status: 'qualified',
|
|
97
|
-
artifact: Object.freeze({ path: artifactRelativePath, absolutePath: artifactPath, schemaId, parentPath:
|
|
104
|
+
artifact: Object.freeze({ path: artifactRelativePath, absolutePath: artifactPath, schemaId, parentPath: parentReference, parentSource: parentSource || '', selfIntegrity: selfIntegrity.state, written: true }),
|
|
98
105
|
qualification: Object.freeze({ audit: audit.status, stage: stage.status, exportReady: Boolean(stage?.stagedArtifact?.qualification?.exportReady) }),
|
|
99
106
|
findingSummary: mergeFindingSummaries(audit?.findingSummary, stage?.findingSummary),
|
|
100
107
|
nextAction: schemaId === 'tiinex.handoff.v1'
|
|
@@ -186,12 +193,62 @@ async function recoverQualifiedLocalSchemaReferenceAuthority(schemaId, context =
|
|
|
186
193
|
});
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
function
|
|
196
|
+
async function allocateArtifactRelativePath({ workspaceRoot, targetDirectory, parentRelativePath, schemaId, title } = {}) {
|
|
197
|
+
const directory = normalizeWorkspaceRelativePath(targetDirectory || (parentRelativePath ? path.posix.dirname(parentRelativePath) : '.topics')) || '.topics';
|
|
198
|
+
const existingPaths = await directoryArtifactPaths(workspaceRoot, directory);
|
|
199
|
+
const allocation = parentRelativePath
|
|
200
|
+
? allocateContinuationPath({ parentRecord: { path: parentRelativePath }, targetId: schemaId, targetLabel: labelFromSchemaId(schemaId), title }, { targetDirectory: directory, existingPaths })
|
|
201
|
+
: allocateDirectoryArtifactPath({ targetDirectory: directory, targetId: schemaId, targetLabel: labelFromSchemaId(schemaId), title }, { existingPaths });
|
|
202
|
+
const allocated = normalizeWorkspaceRelativePath(allocation?.path || '');
|
|
203
|
+
if (!allocated) throw new Error('portable.cli.author.allocation.unavailable');
|
|
204
|
+
return allocated;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function directoryArtifactPaths(workspaceRoot, directory) {
|
|
208
|
+
const dirPath = safeWorkspaceDirectory(workspaceRoot, directory);
|
|
209
|
+
let entries = [];
|
|
210
|
+
try { entries = await readdir(dirPath, { withFileTypes: true }); }
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error?.code === 'ENOENT') return [];
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => `${directory}/${entry.name}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function safeWorkspaceDirectory(root, relative) {
|
|
219
|
+
const normalized = normalizeWorkspaceRelativePath(relative || '.topics') || '.topics';
|
|
220
|
+
const target = path.resolve(root, normalized);
|
|
221
|
+
const rel = path.relative(root, target);
|
|
222
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error(`portable.cli.author.directory.unsafe:${relative}`);
|
|
223
|
+
return target;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function labelFromSchemaId(id = '') {
|
|
227
|
+
const tail = String(id || '').split('.').filter(Boolean).slice(-2, -1)[0] || String(id || 'artifact');
|
|
228
|
+
return tail.charAt(0).toUpperCase() + tail.slice(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveParentReference(flags = {}, state = {}) {
|
|
190
232
|
if (flags['no-parent']) return '';
|
|
191
|
-
if (typeof flags.parent === 'string' && flags.parent.trim()) return
|
|
233
|
+
if (typeof flags.parent === 'string' && flags.parent.trim()) return normalizeParentReference(flags.parent);
|
|
192
234
|
return normalizeWorkspaceRelativePath(state?.lastAuthoredPath || state?.selectedHandoffPath || '');
|
|
193
235
|
}
|
|
194
236
|
|
|
237
|
+
export function normalizeParentReference(value = '') {
|
|
238
|
+
const raw = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
239
|
+
if (!raw || raw.startsWith('/') || /^[A-Za-z]:\//.test(raw)) return '';
|
|
240
|
+
const marker = raw.indexOf('::');
|
|
241
|
+
if (marker < 0) return normalizeWorkspaceRelativePath(raw);
|
|
242
|
+
const workspaceId = raw.slice(0, marker).trim();
|
|
243
|
+
const innerPath = normalizeWorkspaceRelativePath(raw.slice(marker + 2));
|
|
244
|
+
if (!workspaceId || !/^[A-Za-z0-9._-]+$/.test(workspaceId) || !innerPath) return '';
|
|
245
|
+
return `${workspaceId}::${innerPath}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isWorkspaceQualifiedReference(value = '') {
|
|
249
|
+
return /^[A-Za-z0-9._-]+::/.test(String(value || ''));
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
function defaultTransition(schemaId, hasParent) {
|
|
196
253
|
if (!hasParent) return 'create-artifact';
|
|
197
254
|
if (schemaId === 'tiinex.evidence.v1') return 'reference-record';
|
|
@@ -28,6 +28,7 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
28
28
|
if (!flags.route && handoffPath) flags.route = handoffPath;
|
|
29
29
|
if (!flags.output && !flags['output-dir'] && parsed.surfaceCommand === 'handoff' && continuationState.returnOutputDir) flags['output-dir'] = continuationState.returnOutputDir;
|
|
30
30
|
const materialBindings = await readOptionalJson(flags['material-bindings'] || flags.materials);
|
|
31
|
+
const packageParentWorkspaceAliases = await readOptionalJson(flags['package-parent-workspace-aliases']);
|
|
31
32
|
const operatorCarrierProfile = await readOptionalJson(flags['carrier-profile']);
|
|
32
33
|
const expectedToolingBootstrap = await readOptionalJson(flags['tooling-bootstrap-manifest']);
|
|
33
34
|
const workspaceDescriptorValue = await readOptionalJson(flags['workspace-roots'] || flags['workspace-descriptors']);
|
|
@@ -124,7 +125,8 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
|
|
|
124
125
|
carrierProfile,
|
|
125
126
|
packageParentBundle,
|
|
126
127
|
packageParentPath: parentPackagePath ? path.resolve(parentPackagePath) : '',
|
|
127
|
-
packageParentSha256
|
|
128
|
+
packageParentSha256,
|
|
129
|
+
packageParentWorkspaceAliases
|
|
128
130
|
}, runtime);
|
|
129
131
|
return {
|
|
130
132
|
input,
|
|
@@ -10,7 +10,7 @@ export function portableCliHelpText(commandPrefix = '', surfaceCommand = '') {
|
|
|
10
10
|
'Common path (same command for humans and LLMs):',
|
|
11
11
|
`${command} ground <handoff-package.zip> --route <Continue-from> [--holder-role <recipient-role>]`,
|
|
12
12
|
`${command} ground <handoff-package.zip> --route <Continue-from> --holder-role <recipient-role> --continue <workspace-dir>`,
|
|
13
|
-
`${command} author <workspace-dir> --schema <schema-id> --path <workspace-relative-artifact> --body <body.md> [--parent <workspace-relative-parent>] [--title <title>] [--summary <summary>] [--why <why>]`,
|
|
13
|
+
`${command} author <workspace-dir> --schema <schema-id> (--path <workspace-relative-artifact> | --directory <workspace-relative-directory>) --body <body.md> [--parent <workspace-relative-or-qualified-parent>] [--parent-source <local-parent-file>] [--title <title>] [--summary <summary>] [--why <why>]`,
|
|
14
14
|
`${command} handoff <workspace-dir>`,
|
|
15
15
|
'',
|
|
16
16
|
`Handoff aliases: ${command} orient <carrier.zip>; ${command} validate <carrier.zip>`,
|
|
@@ -20,7 +20,7 @@ export function portableCliHelpText(commandPrefix = '', surfaceCommand = '') {
|
|
|
20
20
|
'- `orient`, `ground`, and public `handoff` use compact decision-first default projections; add `--full` on the same command for the complete qualified receipt.',
|
|
21
21
|
'- `ground` is read-only; append `--continue <workspace-dir>` only after `grounded-to-act` to materialize the selected carried Workspace and runtime-only `.tiinex/continuation.json`.',
|
|
22
22
|
'- `author` uses continuation state to infer ordinary Parent continuity, seal c14n-v2 integrity, audit, and stage; invalid artifacts are not retained.',
|
|
23
|
-
'- `handoff` uses continuation state plus the latest qualified authored Handoff to manufacture the canonical return carrier and excludes runtime-only `.tiinex` state. Normal operator completion is one Handoff package plus the exact routing text; markdown-capable hosts render that routing in a fenced code block, and do not emit loose Evidence/Handoff markdown as extra transport payloads.',
|
|
23
|
+
'- `handoff` uses continuation state plus the latest qualified authored Handoff to manufacture the canonical return carrier and excludes runtime-only `.tiinex` state. When a package parent carries an older Workspace id that has been explicitly renamed, advanced manufacture may bind that predecessor id to a supplied current Workspace with `--package-parent-workspace-aliases`; aliases never infer source identity. Normal operator completion is one Handoff package plus the exact routing text; markdown-capable hosts render that routing in a fenced code block, and do not emit loose Evidence/Handoff markdown as extra transport payloads.',
|
|
24
24
|
'- Remote reads/writes remain explicit host concerns. Tooling operation safety does not create or revoke semantic Task/Handoff authority.',
|
|
25
25
|
'',
|
|
26
26
|
'Use `<common-command> --help` for focused common-path usage. Use `operations` deliberately for the advanced/internal operation catalog.'
|
|
@@ -43,9 +43,9 @@ function commonCommandHelp(command, surfaceCommand) {
|
|
|
43
43
|
if (surfaceCommand === 'author') return [
|
|
44
44
|
'Tiinex portable tooling — author',
|
|
45
45
|
'',
|
|
46
|
-
`${command} author <workspace-dir> --schema <schema-id> --path <workspace-relative-artifact> --body <body.md> [--parent <workspace-relative-parent>] [--title <title>] [--summary <summary>] [--why <why>]`,
|
|
46
|
+
`${command} author <workspace-dir> --schema <schema-id> (--path <workspace-relative-artifact> | --directory <workspace-relative-directory>) --body <body.md> [--parent <workspace-relative-or-qualified-parent>] [--parent-source <local-parent-file>] [--title <title>] [--summary <summary>] [--why <why>]`,
|
|
47
47
|
'',
|
|
48
|
-
'Uses qualified continuation state to infer the ordinary Parent when `--parent` is omitted
|
|
48
|
+
'Uses qualified continuation state to infer the ordinary Parent when `--parent` is omitted. Supply `--path` for an exact requested coordinate or `--directory` to let Tooling allocate inside that directory-local filename namespace. For a Workspace-qualified Parent such as `business::.topics/...`, also supply `--parent-source` so Tooling reads and seals against the exact Parent bytes without treating the foreign address as a local path. Authoring seals c14n-v2 self-integrity, audits, stages, and updates continuation state only after qualification. Invalid output is not retained.',
|
|
49
49
|
'',
|
|
50
50
|
`Advanced/internal catalog: ${command} operations`
|
|
51
51
|
];
|
|
@@ -2,5 +2,6 @@ export const OPERATIONS_WITHOUT_EXPLICIT_MATERIAL = new Set([
|
|
|
2
2
|
'prepare-task','prepare-materialization','create-local-artifact-set','create-local-draft','plan-host-action','accept-host-receipt',
|
|
3
3
|
'describe-checkpoint-gate','qualify-checkpoint','describe-schema-chain','schema-guide','plan-artifact','list-material-providers',
|
|
4
4
|
'resolve-schema-material','resolve-schema-chain-material','materialize-durable-findings','build-runtime-package','roundtrip-runtime-package',
|
|
5
|
+
'compare-source-frontiers',
|
|
5
6
|
'describe-cold-start-ingress','project-cold-start-host','qualify-cold-start','ground-cold-consumer'
|
|
6
7
|
]);
|
|
@@ -3,6 +3,19 @@ export async function prepareOperatorBridgeCliInput(command = '', material = {},
|
|
|
3
3
|
const repositories = await readOptionalJson(flags.repositories);
|
|
4
4
|
return { input: { ...material, repositories: repositories.repositories || repositories }, options: {} };
|
|
5
5
|
}
|
|
6
|
+
if (command === 'compare-source-frontiers') {
|
|
7
|
+
return {
|
|
8
|
+
input: {
|
|
9
|
+
leftKind: flags['left-kind'] || flags.leftKind || '',
|
|
10
|
+
left: flags.left || '',
|
|
11
|
+
leftId: flags['left-id'] || flags.leftId || '',
|
|
12
|
+
rightKind: flags['right-kind'] || flags.rightKind || '',
|
|
13
|
+
right: flags.right || '',
|
|
14
|
+
rightSelect: flags['right-select'] || flags.rightSelect || ''
|
|
15
|
+
},
|
|
16
|
+
options: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
6
19
|
if (command === 'project-handoff-authoring-plan') return { input: { ...material, parentPath: flags.parent || flags['parent-path'] || '', title: flags.title || '' }, options: {} };
|
|
7
20
|
if (command === 'project-handoff-endpoints') return { input: { ...material, workspaceId: flags['workspace-id'] || flags.workspace || 'workspace' }, options: {} };
|
|
8
21
|
if (command === 'project-operator-context') {
|
|
@@ -67,7 +67,8 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
67
67
|
bundle: input.packageParentBundle || null,
|
|
68
68
|
currentWorkspaceIds: [...seenWorkspaceIds],
|
|
69
69
|
parentPackagePath: input.packageParentPath || '',
|
|
70
|
-
parentPackageSha256: input.packageParentSha256 || ''
|
|
70
|
+
parentPackageSha256: input.packageParentSha256 || '',
|
|
71
|
+
workspaceAliases: input.packageParentWorkspaceAliases || input.workspaceAliases || {}
|
|
71
72
|
});
|
|
72
73
|
const additionalEnumerationsPromise = Promise.all(additionalWorkspaceInputs.map(async ({ descriptor, id, root, requestedTitle }) => {
|
|
73
74
|
const enumerated = await enumerateNodeWorkspace(root, {
|
|
@@ -168,6 +169,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
|
|
|
168
169
|
state: String(packageParentReuse.state || ''),
|
|
169
170
|
inspectionStatus: String(packageParentReuse.inspectionStatus || ''),
|
|
170
171
|
inheritedWorkspaceIds: Object.freeze((packageParentReuse.inherited || []).map((item) => String(item.id || ''))),
|
|
172
|
+
workspaceAliases: Object.freeze([...(packageParentReuse.workspaceAliases || [])].map((item) => Object.freeze({ ...item }))),
|
|
171
173
|
boundary: String(packageParentReuse.boundary || '')
|
|
172
174
|
}),
|
|
173
175
|
carrierProjection: Object.freeze({ requestedRoutes: transportRoutes.length || 1, carrierLineage: normalizeHandoffCarrierLineage(input.carrierLineage || null), carrierProfile: normalizeHandoffCarrierProfile(input.carrierProfile || null), boundary: 'Routes are qualified later against packaged workspace bytes; adapter text is not authority.' })
|
|
@@ -6,11 +6,13 @@ export function preparePackageParentWorkspaceReuse(input = {}) {
|
|
|
6
6
|
const bundle = input.bundle || null;
|
|
7
7
|
if (!bundle?.files?.length) return emptyReuse('unavailable');
|
|
8
8
|
const currentIds = new Set([...(input.currentWorkspaceIds || [])].map(normalizeId).filter(Boolean));
|
|
9
|
+
const workspaceAliases = normalizePackageParentWorkspaceAliases(input.workspaceAliases || input.packageParentWorkspaceAliases || {});
|
|
10
|
+
for (const alias of workspaceAliases.values()) if (!currentIds.has(alias)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-alias.target-unresolved:${alias}`);
|
|
9
11
|
const inspection = inspectRecipientFacingV2Topology(bundle);
|
|
10
12
|
const declared = declaredPackageWorkspaceBindings(bundle, inspection);
|
|
11
13
|
if (!declared.length) return emptyReuse('unsupported-parent-surface');
|
|
12
14
|
if (inspection.status !== 'valid') throw new Error('portable.handoff-manufacture.package-parent.workspace-provider.invalid');
|
|
13
|
-
const missing = declared.filter((item) => !
|
|
15
|
+
const missing = declared.filter((item) => !packageParentWorkspaceSupersededByCurrent(item.workspaceId, currentIds, workspaceAliases));
|
|
14
16
|
if (!missing.length) return emptyReuse('not-needed');
|
|
15
17
|
|
|
16
18
|
const providerById = new Map((inspection.workspaceByteProvider?.workspaces || []).map((item) => [normalizeId(item.id), item]));
|
|
@@ -39,7 +41,8 @@ export function preparePackageParentWorkspaceReuse(input = {}) {
|
|
|
39
41
|
workspaceTargets: Object.freeze(workspaceTargets),
|
|
40
42
|
inspectionStatus: inspection.status,
|
|
41
43
|
missingWorkspaceIds: Object.freeze(missing.map((item) => normalizeId(item.workspaceId))),
|
|
42
|
-
|
|
44
|
+
workspaceAliases: Object.freeze([...workspaceAliases.entries()].map(([parentWorkspaceId, currentWorkspaceId]) => Object.freeze({ parentWorkspaceId, currentWorkspaceId }))),
|
|
45
|
+
boundary: 'Exact complete Workspace bytes reused from one independently qualified received package parent. Explicit current Workspace roots take precedence by id, and explicit qualified workspace aliases may supersede a renamed parent-carrier Workspace without carrying stale duplicate source. Parent-carrier placement and lineage remain non-semantic.'
|
|
43
46
|
});
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -121,8 +124,34 @@ function buildInheritedEnumeration(provider = {}, source = {}) {
|
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
export function normalizePackageParentWorkspaceAliases(value = {}) {
|
|
128
|
+
const entries = Array.isArray(value)
|
|
129
|
+
? value.map((item) => [item?.parentWorkspaceId || item?.parent || item?.from, item?.currentWorkspaceId || item?.current || item?.to])
|
|
130
|
+
: Object.entries(value || {});
|
|
131
|
+
const out = new Map();
|
|
132
|
+
const targets = new Set();
|
|
133
|
+
for (const [parentValue, currentValue] of entries) {
|
|
134
|
+
const parentWorkspaceId = normalizeId(parentValue);
|
|
135
|
+
const currentWorkspaceId = normalizeId(currentValue);
|
|
136
|
+
if (!parentWorkspaceId || !currentWorkspaceId) throw new Error('portable.handoff-manufacture.package-parent.workspace-alias.invalid');
|
|
137
|
+
if (parentWorkspaceId === currentWorkspaceId) throw new Error(`portable.handoff-manufacture.package-parent.workspace-alias.identity:${parentWorkspaceId}`);
|
|
138
|
+
if (out.has(parentWorkspaceId)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-alias.duplicate-parent:${parentWorkspaceId}`);
|
|
139
|
+
if (targets.has(currentWorkspaceId)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-alias.duplicate-target:${currentWorkspaceId}`);
|
|
140
|
+
out.set(parentWorkspaceId, currentWorkspaceId);
|
|
141
|
+
targets.add(currentWorkspaceId);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function packageParentWorkspaceSupersededByCurrent(parentWorkspaceId = '', currentWorkspaceIds = new Set(), workspaceAliases = new Map()) {
|
|
147
|
+
const id = normalizeId(parentWorkspaceId);
|
|
148
|
+
if (currentWorkspaceIds.has(id)) return true;
|
|
149
|
+
const replacement = workspaceAliases.get(id) || '';
|
|
150
|
+
return Boolean(replacement && currentWorkspaceIds.has(replacement));
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
function emptyReuse(state) {
|
|
125
|
-
return Object.freeze({ state, inherited: Object.freeze([]), workspaceTargets: Object.freeze([]), inspectionStatus: '', missingWorkspaceIds: Object.freeze([]), boundary: 'No package-parent Workspace provider reuse was required.' });
|
|
154
|
+
return Object.freeze({ state, inherited: Object.freeze([]), workspaceTargets: Object.freeze([]), inspectionStatus: '', missingWorkspaceIds: Object.freeze([]), workspaceAliases: Object.freeze([]), boundary: 'No package-parent Workspace provider reuse was required.' });
|
|
126
155
|
}
|
|
127
156
|
function normalizeId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''); }
|
|
128
157
|
function mediaTypeForPath(value = '') { const lower = String(value || '').toLowerCase(); if (lower.endsWith('.md')) return 'text/markdown'; if (lower.endsWith('.json')) return 'application/json'; if (/\.(?:m?js|cjs)$/.test(lower)) return 'text/javascript'; if (lower.endsWith('.ts')) return 'text/typescript'; if (lower.endsWith('.css')) return 'text/css'; if (lower.endsWith('.html')) return 'text/html'; if (/\.(?:yml|yaml)$/.test(lower)) return 'text/yaml'; if (lower.endsWith('.txt')) return 'text/plain'; return 'application/octet-stream'; }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { access, readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { sha256Hex } from '../../../export/package.bytes.js';
|
|
4
|
+
import { inspectStoredWorkspaceArchive } from './workspaceByteProvider.js';
|
|
5
|
+
import { parseHandoffPackageV1 } from './recipientV2.packageV1.contract.js';
|
|
6
|
+
import { currentSchemaId, decodeUtf8, deepFreeze, dedupeFindings } from './recipientV2.packageV1.shared.js';
|
|
7
|
+
import { finding } from './recipientV2.topology.materials.js';
|
|
8
|
+
|
|
9
|
+
export async function compareSourceFrontiers(input = {}) {
|
|
10
|
+
const leftKind = String(input.leftKind || '').trim();
|
|
11
|
+
const rightKind = String(input.rightKind || '').trim();
|
|
12
|
+
const leftRoot = String(input.left || '').trim();
|
|
13
|
+
const rightPath = String(input.right || '').trim();
|
|
14
|
+
const workspaceId = String(input.rightSelect || input.leftId || '').trim();
|
|
15
|
+
const findings = [];
|
|
16
|
+
|
|
17
|
+
if (leftKind !== 'local-workspace') findings.push(finding('error', 'portable.source-frontier.left-kind-invalid', 'Source frontier comparison requires a local workspace on the left side.', { observed: leftKind }));
|
|
18
|
+
if (rightKind !== 'handoff-package') findings.push(finding('error', 'portable.source-frontier.right-kind-invalid', 'Source frontier comparison requires a handoff package on the right side.', { observed: rightKind }));
|
|
19
|
+
if (!leftRoot) findings.push(finding('error', 'portable.source-frontier.left-missing', 'Source frontier comparison requires a local workspace root.', { side: 'left' }));
|
|
20
|
+
if (!rightPath) findings.push(finding('error', 'portable.source-frontier.right-missing', 'Source frontier comparison requires a handoff package path.', { side: 'right' }));
|
|
21
|
+
if (!workspaceId) findings.push(finding('error', 'portable.source-frontier.workspace-id-missing', 'Source frontier comparison requires a selected workspace id.', { side: 'right-select' }));
|
|
22
|
+
if (findings.length) return blocked(findings);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await access(leftRoot);
|
|
26
|
+
} catch {
|
|
27
|
+
findings.push(finding('error', 'portable.source-frontier.left-unavailable', 'The local workspace root is not available.', { path: leftRoot }));
|
|
28
|
+
return blocked(findings);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let packageBytes;
|
|
32
|
+
try {
|
|
33
|
+
packageBytes = await readFile(rightPath);
|
|
34
|
+
} catch {
|
|
35
|
+
findings.push(finding('error', 'portable.source-frontier.right-unavailable', 'The handoff package path is not available.', { path: rightPath }));
|
|
36
|
+
return blocked(findings);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packageArchive = inspectStoredWorkspaceArchive(packageBytes, { ownedBytes: true });
|
|
40
|
+
if (packageArchive.state !== 'qualified') {
|
|
41
|
+
findings.push(finding('error', 'portable.source-frontier.package-unqualified', 'The supplied handoff package is not a qualified stored ZIP archive.', { path: rightPath }));
|
|
42
|
+
findings.push(...packageArchive.findings);
|
|
43
|
+
return blocked(findings);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rootEntry = (packageArchive.entries || []).find((entry) => currentSchemaId(decodeUtf8(entry.data)) === 'tiinex.handoff.package.v1') || null;
|
|
47
|
+
if (!rootEntry) {
|
|
48
|
+
findings.push(finding('error', 'portable.source-frontier.package-root-missing', 'The handoff package does not expose a readable package-v1 root artifact.', { path: rightPath }));
|
|
49
|
+
return blocked(findings);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const packageRoot = parseHandoffPackageV1(decodeUtf8(rootEntry.data));
|
|
53
|
+
const binding = (packageRoot.workspaces || []).find((item) => String(item.workspaceId || '') === workspaceId) || null;
|
|
54
|
+
if (!binding) {
|
|
55
|
+
findings.push(finding('error', 'portable.source-frontier.workspace-missing', 'The selected workspace id is not bound by the supplied handoff package.', { workspaceId }));
|
|
56
|
+
return blocked(findings);
|
|
57
|
+
}
|
|
58
|
+
if (!binding.snapshotPath) {
|
|
59
|
+
findings.push(finding('error', 'portable.source-frontier.snapshot-path-missing', 'The selected workspace binding does not declare a snapshot path.', { workspaceId }));
|
|
60
|
+
return blocked(findings);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const packageEntries = indexEntries(packageArchive.entries || []);
|
|
64
|
+
const snapshotEntry = packageEntries.get(binding.snapshotPath) || null;
|
|
65
|
+
if (!snapshotEntry) {
|
|
66
|
+
findings.push(finding('error', 'portable.source-frontier.snapshot-missing', 'The selected workspace snapshot is not present in the supplied handoff package.', { workspaceId, path: binding.snapshotPath }));
|
|
67
|
+
return blocked(findings);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const snapshotArchive = inspectStoredWorkspaceArchive(snapshotEntry.data, { ownedBytes: true });
|
|
71
|
+
if (snapshotArchive.state !== 'qualified') {
|
|
72
|
+
findings.push(finding('error', 'portable.source-frontier.snapshot-unqualified', 'The selected workspace snapshot is not a qualified stored ZIP archive.', { workspaceId, path: binding.snapshotPath }));
|
|
73
|
+
findings.push(...snapshotArchive.findings);
|
|
74
|
+
return blocked(findings);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const localFiles = await collectLocalFiles(leftRoot);
|
|
78
|
+
const snapshotFiles = collectArchiveFiles(snapshotArchive.entries || []);
|
|
79
|
+
const comparison = compareFileSets(localFiles, snapshotFiles);
|
|
80
|
+
const state = comparison.counts.total === 0 ? 'exact' : 'changed';
|
|
81
|
+
|
|
82
|
+
return deepFreeze({
|
|
83
|
+
schema: 'tiinex.portable.source-frontier.compare.v1',
|
|
84
|
+
status: 'ready',
|
|
85
|
+
state,
|
|
86
|
+
mode: 'two-way',
|
|
87
|
+
workspaces: Object.freeze([
|
|
88
|
+
Object.freeze({
|
|
89
|
+
workspaceId,
|
|
90
|
+
state,
|
|
91
|
+
delta: Object.freeze({
|
|
92
|
+
counts: Object.freeze(comparison.counts),
|
|
93
|
+
added: Object.freeze(comparison.added),
|
|
94
|
+
removed: Object.freeze(comparison.removed),
|
|
95
|
+
byteChanged: Object.freeze(comparison.byteChanged)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
]),
|
|
99
|
+
findings: Object.freeze([]),
|
|
100
|
+
boundary: 'Compares one local workspace root against the exact carried workspace snapshot bytes bound by a recipient-facing handoff package. It reports only file addition, removal, and byte-change counts and fails closed when the selected workspace binding or snapshot archive is unavailable.'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function blocked(findings = []) {
|
|
105
|
+
const all = dedupeFindings(findings);
|
|
106
|
+
return deepFreeze({
|
|
107
|
+
schema: 'tiinex.portable.source-frontier.compare.v1',
|
|
108
|
+
status: 'blocked',
|
|
109
|
+
state: 'blocked',
|
|
110
|
+
mode: 'two-way',
|
|
111
|
+
workspaces: Object.freeze([]),
|
|
112
|
+
findings: Object.freeze(all),
|
|
113
|
+
boundary: 'Source frontier comparison failed closed before any comparison result was emitted.'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function collectLocalFiles(root, current = root, prefix = '') {
|
|
118
|
+
const files = [];
|
|
119
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!prefix && entry.name === '.git') continue;
|
|
122
|
+
const relative = normalizePath(prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
123
|
+
const absolute = path.join(current, entry.name);
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
const nested = await collectLocalFiles(root, absolute, relative);
|
|
126
|
+
files.push(...nested);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!entry.isFile()) continue;
|
|
130
|
+
const data = await readFile(absolute);
|
|
131
|
+
files.push(Object.freeze({ path: relative, bytes: data.byteLength, sha256: sha256Hex(data) }));
|
|
132
|
+
}
|
|
133
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectArchiveFiles(entries = []) {
|
|
137
|
+
return entries
|
|
138
|
+
.filter((entry) => !isIgnoredPath(entry.path || ''))
|
|
139
|
+
.map((entry) => Object.freeze({ path: normalizePath(entry.path || ''), bytes: Number(entry.bytes || 0), sha256: String(entry.sha256 || '').toLowerCase() }))
|
|
140
|
+
.filter((entry) => entry.path)
|
|
141
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function compareFileSets(left = [], right = []) {
|
|
145
|
+
const leftByPath = new Map(left.map((entry) => [entry.path, entry]));
|
|
146
|
+
const rightByPath = new Map(right.map((entry) => [entry.path, entry]));
|
|
147
|
+
const paths = [...new Set([...leftByPath.keys(), ...rightByPath.keys()])].sort();
|
|
148
|
+
const added = [];
|
|
149
|
+
const removed = [];
|
|
150
|
+
const byteChanged = [];
|
|
151
|
+
|
|
152
|
+
for (const filePath of paths) {
|
|
153
|
+
const local = leftByPath.get(filePath) || null;
|
|
154
|
+
const remote = rightByPath.get(filePath) || null;
|
|
155
|
+
if (!local && remote) { added.push(filePath); continue; }
|
|
156
|
+
if (local && !remote) { removed.push(filePath); continue; }
|
|
157
|
+
if (local && remote && (local.bytes !== remote.bytes || local.sha256 !== remote.sha256)) byteChanged.push(filePath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
added,
|
|
162
|
+
removed,
|
|
163
|
+
byteChanged,
|
|
164
|
+
counts: {
|
|
165
|
+
added: added.length,
|
|
166
|
+
removed: removed.length,
|
|
167
|
+
byteChanged: byteChanged.length,
|
|
168
|
+
total: added.length + removed.length + byteChanged.length
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function indexEntries(entries = []) {
|
|
174
|
+
const index = new Map();
|
|
175
|
+
for (const entry of entries) index.set(normalizePath(entry.path || ''), entry);
|
|
176
|
+
return index;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizePath(value = '') {
|
|
180
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/g, '').split('/').filter((part) => part && part !== '.').join('/');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isIgnoredPath(value = '') {
|
|
184
|
+
const normalized = normalizePath(value);
|
|
185
|
+
return !normalized || normalized === '.git' || normalized.startsWith('.git/');
|
|
186
|
+
}
|
|
@@ -11,6 +11,7 @@ import { projectPortableEditorAssistance } from './editor/editor.assistance.js';
|
|
|
11
11
|
import { projectQualifiedHandoffLeaves } from './handoff/handoffLeafProjection.js';
|
|
12
12
|
import { projectPortableAuthoringParent } from './editor/authoring.parent.js';
|
|
13
13
|
import { projectQualifiedWorkspacePackageSources } from './handoff/workspacePackageSources.js';
|
|
14
|
+
import { compareSourceFrontiers } from './handoff/sourceFrontierComparison.js';
|
|
14
15
|
import { projectPortableHandoffAuthoringPlan } from './handoff/handoffAuthoringPlan.js';
|
|
15
16
|
import { projectQualifiedHandoffEndpoints } from './handoff/handoffEndpointProjection.js';
|
|
16
17
|
import { projectPortableOperatorContext } from './handoff/operatorContextProjection.js';
|
|
@@ -95,6 +96,13 @@ export function createPortablePackageOperationEntries({ operation, wrapPortableR
|
|
|
95
96
|
inputSchema: 'tiinex.portable.input.v1',
|
|
96
97
|
handler: (input = {}) => wrapPortableResult('project-workspace-package-sources', projectQualifiedWorkspacePackageSources(input))
|
|
97
98
|
}),
|
|
99
|
+
'compare-source-frontiers': operation({
|
|
100
|
+
name: 'compare-source-frontiers',
|
|
101
|
+
description: 'Compare one local Workspace root against one carried workspace snapshot bound by a recipient-facing Handoff package.',
|
|
102
|
+
safety: 'read-only',
|
|
103
|
+
inputSchema: 'tiinex.portable.input.v1',
|
|
104
|
+
handler: async (input = {}) => wrapPortableResult('compare-source-frontiers', await compareSourceFrontiers(input))
|
|
105
|
+
}),
|
|
98
106
|
'project-handoff-authoring-plan': operation({
|
|
99
107
|
name: 'project-handoff-authoring-plan',
|
|
100
108
|
description: 'Project the shared root or continuation path allocation for native Handoff authoring from exact local material without creating an artifact.',
|
|
@@ -167,29 +167,59 @@ export function allocateContinuationPath({ parentRecord = {}, targetId = '', tar
|
|
|
167
167
|
if (explicitPath) return { path: uniqueTransitionPath(explicitPath, occupied), policy: pathPolicyForExplicit(explicitPath) };
|
|
168
168
|
const parentPath = externalWebArtifactUrl(parentRecord) ? '' : canonicalLocalPath(parentRecord.path || parentRecord.sourcePath || parentRecord.sourceTarget?.sourceArtifactPath || '');
|
|
169
169
|
const parentDir = parentDirectory(parentPath) || '.topics';
|
|
170
|
+
const requestedDir = canonicalLocalPath(options.targetDirectory || options.directory || '');
|
|
171
|
+
const targetDir = requestedDir || parentDir;
|
|
170
172
|
const parentPrefix = lineagePrefixFromPath(parentPath);
|
|
171
173
|
const labelSlug = slugify(title || parentRecord.title || targetLabel || 'continuation');
|
|
172
174
|
const targetSlug = slugify(targetLabel || labelFromSchemaId(targetId) || 'leaf');
|
|
173
175
|
const extension = '.trace.md';
|
|
176
|
+
const directoryLocal = Boolean(requestedDir && requestedDir !== parentDir);
|
|
174
177
|
const policy = {
|
|
175
178
|
schema: 'tiinex.transition.path-policy.v1',
|
|
176
|
-
kind: 'same-parent-directory',
|
|
179
|
+
kind: directoryLocal ? 'directory-local-continuation' : 'same-parent-directory',
|
|
177
180
|
parentDirectory: parentDir,
|
|
181
|
+
targetDirectory: targetDir,
|
|
178
182
|
parentPath,
|
|
179
183
|
parentLineagePrefix: parentPrefix,
|
|
180
184
|
labelSlug,
|
|
181
185
|
targetSlug,
|
|
182
|
-
extension
|
|
186
|
+
extension,
|
|
187
|
+
allocationAuthority: directoryLocal ? 'target-directory-local-namespace' : 'same-parent-directory-continuation'
|
|
188
|
+
};
|
|
189
|
+
return { path: pathFromPolicy(policy, occupied), policy };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function allocateDirectoryArtifactPath({ targetDirectory = '.topics', targetId = '', targetLabel = '', title = '' } = {}, options = {}) {
|
|
193
|
+
const occupied = existingTransitionPaths(options);
|
|
194
|
+
const explicitPath = canonicalLocalPath(options.path || options.draftPath || '');
|
|
195
|
+
if (explicitPath) return { path: uniqueTransitionPath(explicitPath, occupied), policy: pathPolicyForExplicit(explicitPath) };
|
|
196
|
+
const dir = canonicalLocalPath(targetDirectory || '.topics') || '.topics';
|
|
197
|
+
const labelSlug = slugify(title || targetLabel || labelFromSchemaId(targetId) || 'artifact');
|
|
198
|
+
const targetSlug = slugify(targetLabel || labelFromSchemaId(targetId) || 'artifact');
|
|
199
|
+
const extension = '.trace.md';
|
|
200
|
+
const policy = {
|
|
201
|
+
schema: 'tiinex.transition.path-policy.v1',
|
|
202
|
+
kind: 'directory-local-root',
|
|
203
|
+
targetDirectory: dir,
|
|
204
|
+
labelSlug,
|
|
205
|
+
targetSlug,
|
|
206
|
+
extension,
|
|
207
|
+
allocationAuthority: 'target-directory-local-namespace'
|
|
183
208
|
};
|
|
184
209
|
return { path: pathFromPolicy(policy, occupied), policy };
|
|
185
210
|
}
|
|
186
211
|
|
|
187
212
|
function pathFromPolicy(policy = {}, occupied = new Set()) {
|
|
188
|
-
const
|
|
213
|
+
const kind = String(policy.kind || '').trim();
|
|
214
|
+
const dir = canonicalLocalPath(policy.targetDirectory || policy.parentDirectory || '.topics') || '.topics';
|
|
189
215
|
const extension = String(policy.extension || '.trace.md').startsWith('.') ? String(policy.extension || '.trace.md') : `.${policy.extension}`;
|
|
190
216
|
const labelSlug = slugify(policy.labelSlug || 'continuation');
|
|
191
217
|
const targetSlug = slugify(policy.targetSlug || 'leaf');
|
|
192
218
|
const parentPrefix = String(policy.parentLineagePrefix || '').trim();
|
|
219
|
+
if (kind === 'directory-local-root' || kind === 'directory-local-continuation') {
|
|
220
|
+
const rootPrefix = nextDirectoryRootLineagePrefix(dir, occupied);
|
|
221
|
+
return `${dir}/${rootPrefix}-${labelSlug}.${extension.replace(/^\./, '')}`;
|
|
222
|
+
}
|
|
193
223
|
if (parentPrefix) {
|
|
194
224
|
const childPrefix = nextChildLineagePrefix(parentPrefix, dir, occupied);
|
|
195
225
|
return `${dir}/${childPrefix}-${labelSlug}.${extension.replace(/^\./, '')}`;
|
|
@@ -226,6 +256,25 @@ function lineagePrefixFromPath(path = '') {
|
|
|
226
256
|
return '';
|
|
227
257
|
}
|
|
228
258
|
|
|
259
|
+
|
|
260
|
+
function nextDirectoryRootLineagePrefix(dir = '', occupied = new Set()) {
|
|
261
|
+
const numbers = [];
|
|
262
|
+
let width = 3;
|
|
263
|
+
for (const value of occupied || []) {
|
|
264
|
+
const canonical = canonicalLocalPath(value);
|
|
265
|
+
if (parentDirectory(canonical) !== dir) continue;
|
|
266
|
+
const name = basenameWithoutKnownMarkdownExtension(canonical);
|
|
267
|
+
const match = name.match(/^(\d+)(?:-|$)/);
|
|
268
|
+
if (!match || /^20\d{2}$/.test(match[1])) continue;
|
|
269
|
+
const root = match[1].split('-')[0];
|
|
270
|
+
if (!/^\d+$/.test(root)) continue;
|
|
271
|
+
numbers.push(Number(root));
|
|
272
|
+
width = Math.max(width, root.length);
|
|
273
|
+
}
|
|
274
|
+
const next = numbers.length ? Math.max(...numbers) + 1 : 1;
|
|
275
|
+
return String(next).padStart(width, '0');
|
|
276
|
+
}
|
|
277
|
+
|
|
229
278
|
function nextChildLineagePrefix(parentPrefix = '', dir = '', occupied = new Set()) {
|
|
230
279
|
const prefix = String(parentPrefix || '').trim();
|
|
231
280
|
const width = childOrdinalWidth(prefix);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
|
|
4
|
-
const pointer = JSON.parse(await readFile(new URL('./tiinex.llm.bootstrap.pointer.json', import.meta.url), 'utf8'));
|
|
5
|
-
const bootstrap = await readFile(new URL('./tiinex.llm.bootstrap.md', import.meta.url), 'utf8');
|
|
6
|
-
|
|
7
|
-
assert.equal(pointer.schema, 'tiinex.portable.bootstrap.pointer.v1');
|
|
8
|
-
assert.equal(pointer.repository, 'Tiinex/site');
|
|
9
|
-
assert.equal(pointer.firstOperations.includes('discover-tooling'), true);
|
|
10
|
-
assert.equal(pointer.firstOperations.includes('plan-host-action'), true);
|
|
11
|
-
assert.equal(pointer.boundary.remoteWrite, false);
|
|
12
|
-
assert.equal(pointer.boundary.sourceMutation, false);
|
|
13
|
-
assert.equal(bootstrap.includes('## Startup Modes'), true);
|
|
14
|
-
assert.equal(bootstrap.includes('Pre-prompt only; no bootstrap source is loaded'), true);
|
|
15
|
-
assert.equal(bootstrap.includes(pointer.bootstrapPath), true);
|
|
16
|
-
assert.equal(bootstrap.includes('do not execute package code'), true);
|
|
17
|
-
assert.equal(bootstrap.includes('## Bind Capabilities To Concrete Host Tools'), true);
|
|
18
|
-
assert.equal(bootstrap.includes('tiinex.portable.host-action-receipt.v1'), true);
|
|
19
|
-
assert.equal(bootstrap.includes('## Manufacture Recipient-Relative Handoff Packages'), true);
|
|
20
|
-
assert.equal(bootstrap.includes('manufacture-handoff-package'), true);
|
|
21
|
-
assert.equal(bootstrap.includes('001-1-READ-BEFORE-PROCEEDING.trace.md'), true);
|
|
22
|
-
assert.equal(bootstrap.includes('001-<package-slug>.trace.md'), true);
|
|
23
|
-
assert.equal(bootstrap.includes('Generated Markdown children declare package-local `Parent` continuity'), true);
|
|
24
|
-
assert.equal(bootstrap.includes('Exact durable source artifacts inside Workspace/cache archives retain their own historical provenance'), true);
|
|
25
|
-
assert.equal(bootstrap.includes('001-0-READ-BEFORE-PROCEEDING.trace.md'), false);
|
|
26
|
-
assert.equal(bootstrap.includes('tiinex.package/START.md'), false);
|
|
27
|
-
assert.equal(bootstrap.includes('orient-handoff-package'), true);
|
|
28
|
-
assert.equal(bootstrap.includes('audit-handoff-package-context'), true);
|
|
29
|
-
assert.equal(bootstrap.includes('inspect ./received --summary --phase-timing'), true);
|
|
30
|
-
assert.equal(bootstrap.includes('audit ./received --summary --phase-timing'), true);
|
|
31
|
-
assert.equal(bootstrap.includes('search-lineage ./received --query \"mobile overflow\" --relation leaf --summary --phase-timing'), true);
|
|
32
|
-
assert.equal(bootstrap.includes('resolve-lineage ./received --depth 3 --direction both --summary --phase-timing'), true);
|
|
33
|
-
assert.equal(bootstrap.includes('npm run tooling:search -- --query'), true);
|
|
34
|
-
assert.equal(bootstrap.includes('--include-legacy-topics'), true);
|
|
35
|
-
assert.equal(bootstrap.includes('--include-legacy-fixtures'), true);
|
|
36
|
-
assert.equal(bootstrap.includes('ground ... --include-required-context all'), true);
|
|
37
|
-
assert.equal(bootstrap.includes('A receipt that omits Required Context bodies is not proof that the model has read those bodies.'), true);
|
|
38
|
-
assert.equal(bootstrap.includes('humanOutput.normalInlineRouting.content'), true);
|
|
39
|
-
assert.equal(bootstrap.includes('sole primary package'), true);
|
|
40
|
-
assert.equal(bootstrap.includes('Filename or co-location under a bootstrap-like path does not grant bootstrap authority.'), true);
|
|
41
|
-
assert.equal(bootstrap.includes('canonical Handoff semantic authoring/validation or a locked canonical package schema'), true);
|
|
42
|
-
|
|
43
|
-
console.log('✓ portable bootstrap pointer and startup-mode contract passed');
|