@tiinex/core 0.3.0 → 0.4.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.
Files changed (41) hide show
  1. package/package.json +8 -6
  2. package/src/public/index.js +21 -0
  3. package/src/public/node.js +1 -0
  4. package/src/release/plan.mjs +18 -5
  5. package/src/release/run.mjs +7 -5
  6. package/src/tooling/portable/adapters/cli/cli.command-input.js +3 -0
  7. package/src/tooling/portable/adapters/cli/cli.common-output.js +23 -0
  8. package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +2 -0
  9. package/src/tooling/portable/adapters/cli/cli.help.js +18 -2
  10. package/src/tooling/portable/adapters/cli/cli.material-policy.js +0 -1
  11. package/src/tooling/portable/adapters/cli/cli.operator-bridge.js +0 -13
  12. package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
  13. package/src/tooling/portable/adapters/cli/cli.source-frontier-comparison.js +50 -0
  14. package/src/tooling/portable/adapters/node/handoff.manufacture.js +14 -0
  15. package/src/tooling/portable/adapters/node/handoff.manufacture.packageParent.js +80 -16
  16. package/src/tooling/portable/adapters/node/handoff.manufacture.requirements.js +44 -11
  17. package/src/tooling/portable/adapters/node/handoff.manufacture.scope.js +96 -1
  18. package/src/tooling/portable/adapters/node/sourceFrontierComparison.js +194 -0
  19. package/src/tooling/portable/comparison/sourceFrontierComparison.js +501 -0
  20. package/src/tooling/portable/grounding/grounding.readiness.js +8 -4
  21. package/src/tooling/portable/grounding/grounding.readiness.support.js +63 -1
  22. package/src/tooling/portable/handoff/contextAudit.js +21 -1
  23. package/src/tooling/portable/handoff/materialClosure.descriptor.js +1 -1
  24. package/src/tooling/portable/handoff/materialClosure.materials.js +1 -1
  25. package/src/tooling/portable/handoff/recipientV2.artifacts.js +1 -1
  26. package/src/tooling/portable/handoff/recipientV2.inspect.helpers.js +6 -1
  27. package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +93 -19
  28. package/src/tooling/portable/handoff/recipientV2.packageV1.contract.js +30 -1
  29. package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +8 -2
  30. package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +122 -10
  31. package/src/tooling/portable/handoff/recipientV2.packageV1.js +2 -1
  32. package/src/tooling/portable/handoff/recipientV2.packageV1.secure.js +87 -0
  33. package/src/tooling/portable/handoff/recipientV2.packageV1.shared.js +9 -1
  34. package/src/tooling/portable/handoff/recipientV2.topology.js +2 -2
  35. package/src/tooling/portable/handoff/recipientV2.topology.materials.js +9 -1
  36. package/src/tooling/portable/handoff/transportEnvelopeV1.js +76 -0
  37. package/src/tooling/portable/index.js +4 -0
  38. package/src/tooling/portable/operation.catalog.js +8 -0
  39. package/src/tooling/portable/operation.catalog.package.js +0 -8
  40. package/src/transport/secureTransportV1.js +339 -0
  41. package/src/tooling/portable/handoff/sourceFrontierComparison.js +0 -186
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiinex/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.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,
@@ -133,7 +133,9 @@
133
133
  "./portable-entry": "./tools/tiinex-portable.mjs",
134
134
  "./schema-bootstrap": "./src/tooling/portable/schema/bootstrap/canonical.pack.js",
135
135
  "./node/release": "./src/release/run.mjs",
136
- "./node/release-policy": "./src/release/policy.mjs"
136
+ "./node/release-policy": "./src/release/policy.mjs",
137
+ "./tooling/portable/comparison/sourceFrontierComparison.js": "./src/tooling/portable/comparison/sourceFrontierComparison.js",
138
+ "./tooling/portable/adapters/node/sourceFrontierComparison.js": "./src/tooling/portable/adapters/node/sourceFrontierComparison.js"
137
139
  },
138
140
  "files": [
139
141
  "src",
@@ -165,12 +167,12 @@
165
167
  "type": "git",
166
168
  "url": "git+https://github.com/Tiinex/core.git"
167
169
  },
168
- "gitHead": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
170
+ "gitHead": "b11f8a0c6204578b17cbbfe90bf9b7602ec2a575",
169
171
  "tiinexRelease": {
170
172
  "policy": "tiinex.master-npm-release.v1",
171
- "sourceCommit": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
172
- "sourceTree": "cbf5f25e875efac11c017d9914efa673bdfc2edb",
173
+ "sourceCommit": "b11f8a0c6204578b17cbbfe90bf9b7602ec2a575",
174
+ "sourceTree": "081d90e1b37c422494491fc3a798e88f1c23d3bd",
173
175
  "repository": "Tiinex/core",
174
- "previousVersion": "0.2.0"
176
+ "previousVersion": "0.3.0"
175
177
  }
176
178
  }
@@ -14,3 +14,24 @@ export {
14
14
  } from './companionResources.js';
15
15
 
16
16
  export { projectSchemaAncestry } from './schemaAncestry.js';
17
+ export {
18
+ SECURE_TRANSPORT_V1_PROFILE,
19
+ SECURE_TRANSPORT_V1_ENVELOPE_CONTRACT,
20
+ qualifySecureTransportV1Envelope,
21
+ sealPasswordWorkspacePayload,
22
+ openPasswordWorkspacePayload,
23
+ replacePasswordWorkspaceRecipients,
24
+ secureTransportV1AuthenticatedMetadata
25
+ } from '../transport/secureTransportV1.js';
26
+
27
+ export {
28
+ PORTABLE_SOURCE_FRONTIER_SCHEMA_ID,
29
+ PORTABLE_SOURCE_FRONTIER_COMPARISON_SCHEMA_ID,
30
+ PORTABLE_SOURCE_FRONTIER_SUMMARY_SCHEMA_ID,
31
+ createPortableSourceFrontier,
32
+ createPortableWorkspaceSnapshot,
33
+ comparePortableSourceFrontiers,
34
+ reconcilePortableSourceFrontiers,
35
+ compareOrReconcilePortableSourceFrontiers,
36
+ projectPortableSourceFrontierComparisonSummary
37
+ } from '../tooling/portable/comparison/sourceFrontierComparison.js';
@@ -1,2 +1,3 @@
1
1
  export * from './index.js';
2
2
  export * from '../tooling/portable/index.js';
3
+ export * from '../tooling/portable/adapters/node/sourceFrontierComparison.js';
@@ -16,13 +16,26 @@ export function releasePlan({pkg,metadata,sourceCommit,messages=[],previousPacka
16
16
  if(metadata?.versions?.[decision.targetVersion])throw Error('release.version.collision');
17
17
  return {policy:RELEASE_POLICY,action:'publish',name:pkg.name,version:decision.targetVersion,previousVersion:last?.version||null,previousCommit:last?.tiinexRelease?.sourceCommit||last?.gitHead||null,sourceCommit,decision};
18
18
  }
19
+ export function publishedArchiveState(metadata,{version,integrity}) {
20
+ const published=metadata?.versions?.[version];
21
+ if(!published)return {status:'absent',published:null};
22
+ if(published.dist?.integrity!==integrity)return {status:'collision',published};
23
+ return {status:'exact',published};
24
+ }
19
25
  export function assertPublishContext(env,repository) {
20
26
  if(env.GITHUB_ACTIONS!=='true'||!['push','workflow_dispatch'].includes(env.GITHUB_EVENT_NAME)||env.GITHUB_REF!=='refs/heads/master'||env.GITHUB_REPOSITORY!==repository)throw Error('release.master-only: publishing requires this repository on refs/heads/master');
21
27
  if(env.TIINEX_ENABLE_NPM_PUBLISH!=='true')throw Error('release.not-enabled');
22
28
  }
23
- export async function readRegistry(name,{fetcher=globalThis.fetch}={}) {
24
- const response=await fetcher(`https://registry.npmjs.org/${encodeURIComponent(name)}`,{headers:{Accept:'application/json'},signal:AbortSignal.timeout(30000)});
25
- if(response.status===404)return {name,versions:{}};
26
- if(!response.ok)throw Error(`release.registry.failure:${response.status}`);
27
- const data=await response.json();if(data.name!==name||!data.versions||typeof data.versions!=='object')throw Error('release.registry.invalid-response');return data;
29
+ export async function readRegistry(name,{fetcher=globalThis.fetch,attempts=1,retryDelayMs=250,sleeper=(ms)=>new Promise(resolve=>setTimeout(resolve,ms))}={}) {
30
+ const count=Math.max(1,Number.isInteger(attempts)?attempts:1);
31
+ for(let attempt=1;attempt<=count;attempt+=1){
32
+ const response=await fetcher(`https://registry.npmjs.org/${encodeURIComponent(name)}`,{headers:{Accept:'application/json','Cache-Control':'no-cache'},signal:AbortSignal.timeout(30000)});
33
+ if(response.status===404){
34
+ if(attempt<count){await sleeper(Math.max(0,retryDelayMs)*attempt);continue;}
35
+ return {name,versions:{}};
36
+ }
37
+ if(!response.ok)throw Error(`release.registry.failure:${response.status}`);
38
+ const data=await response.json();if(data.name!==name||!data.versions||typeof data.versions!=='object')throw Error('release.registry.invalid-response');return data;
39
+ }
40
+ return {name,versions:{}};
28
41
  }
@@ -2,7 +2,7 @@ import {readFile,writeFile,mkdir,rm,cp,realpath,stat} from 'node:fs/promises';
2
2
  import {spawnSync} from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import {createHash} from 'node:crypto';
5
- import {assertPublishContext,readRegistry,releasePlan,compareVersion} from './plan.mjs';
5
+ import {assertPublishContext,readRegistry,releasePlan,compareVersion,publishedArchiveState} from './plan.mjs';
6
6
  import {parseSemver} from './policy.mjs';
7
7
  const npm=process.platform==='win32'?'npm.cmd':'npm';
8
8
  function run(command,args,cwd,{allowFailure=false}={}) {const r=spawnSync(command,args,{cwd,encoding:'utf8',timeout:180000,maxBuffer:32*1024*1024,shell:process.platform==='win32'&&command===npm});if(r.error||(!allowFailure&&r.status!==0))throw Error(`${command} ${args[0]} failed: ${r.error?.message||r.stderr||r.stdout}`);return r;}
@@ -16,6 +16,7 @@ export async function runRelease({cwd=process.cwd(),argv=process.argv.slice(2),e
16
16
  const pkg=JSON.parse(await readFile(path.join(cwd,'package.json'),'utf8'));
17
17
  const policy=JSON.parse(await readFile(path.join(cwd,'.github/release-policy.json'),'utf8'));
18
18
  const repository=policy.repository;
19
+ const registryReadOptions=command==='bootstrap'?{}:env.GITHUB_ACTIONS==='true'?{attempts:6,retryDelayMs:500}:{};
19
20
  if(pkg.repository?.url!==`git+https://github.com/${repository}.git`||!repository.startsWith('Tiinex/'))throw Error('release.repository.mismatch');
20
21
  const git=(args,opts)=>run('git',args,cwd,opts);
21
22
  const head=git(['rev-parse','HEAD']).stdout.trim();
@@ -26,18 +27,19 @@ export async function runRelease({cwd=process.cwd(),argv=process.argv.slice(2),e
26
27
  if(p.action==='skip'){console.log(JSON.stringify(p));return p;}
27
28
  const file=path.resolve(cwd,p.file);if(path.dirname(file)!==path.join(cwd,'.release'))throw Error('release.archive-path');
28
29
  const bytes=await readFile(file);if(createHash('sha512').update(bytes).digest('base64')!==p.integrity.slice(7))throw Error('release.archive-tampered');
29
- const meta=await readRegistry(pkg.name);const existing=meta.versions[p.version];
30
- if(existing){if(existing.dist?.integrity!==p.integrity)throw Error('release.version.collision');console.log('Already published exact archive');return p;}
30
+ const meta=await readRegistry(pkg.name,{...registryReadOptions,attempts:8});const existingState=publishedArchiveState(meta,{version:p.version,integrity:p.integrity});
31
+ if(existingState.status==='collision')throw Error('release.version.collision');
32
+ if(existingState.status==='exact'){console.log('Already published exact archive');return p;}
31
33
  const latest=Object.values(meta.versions).filter(v=>!parseSemver(v.version).prerelease).sort((a,b)=>compareVersion(a.version,b.version)).at(-1);
32
34
  if(latest&&compareVersion(latest.version,p.version)>=0)throw Error('release.newer-version-already-published');
33
35
  const result=run(npm,['publish',file,'--ignore-scripts','--access','public','--tag','latest','--provenance'],cwd,{allowFailure:true});
34
- if(result.status!==0){const check=await readRegistry(pkg.name);if(check.versions[p.version]?.dist?.integrity!==p.integrity)throw Error('release.publish.failed: '+result.stderr);}
36
+ if(result.status!==0){const check=await readRegistry(pkg.name,{...registryReadOptions,attempts:10,retryDelayMs:750});const failedState=publishedArchiveState(check,{version:p.version,integrity:p.integrity});if(failedState.status==='collision')throw Error('release.version.collision-after-publish');if(failedState.status!=='exact')throw Error('release.publish.failed: '+result.stderr);}
35
37
  console.log(JSON.stringify({...p,published:true}));return p;
36
38
  }
37
39
  if(command==='prepare') {assertPublishContext(env,repository);if(env.GITHUB_SHA!==head)throw Error('release.checkout-is-not-event-commit');}
38
40
  if(command==='bootstrap' && git(['branch','--show-current']).stdout.trim()!=='master')throw Error('release.bootstrap.master-only');
39
41
  if(git(['status','--porcelain','--untracked-files=no']).stdout.trim())throw Error('release.dirty-source');
40
- const metadata=await readRegistry(pkg.name);
42
+ const metadata=await readRegistry(pkg.name,registryReadOptions);
41
43
  if(command==='bootstrap'&&Object.keys(metadata.versions).length)throw Error('Package exists: configure OIDC and use the master workflow, not bootstrap.');
42
44
  if(command==='bootstrap') { const check=pkg.scripts?.validate?'validate':pkg.scripts?.check?'check':'test'; run(npm,['run',check],cwd); }
43
45
  const previous=Object.values(metadata.versions).filter(v=>!parseSemver(v.version).prerelease).sort((a,b)=>compareVersion(a.version,b.version)).at(-1);
@@ -10,6 +10,7 @@ import { prepareQualifyColdStartCommandInput } from './cli.cold-start-input.js';
10
10
  import { reductionPreflightCliInput } from './cli.reduction-input.js';
11
11
  import {land} from './cli.land.js';
12
12
  import { OPERATIONS_WITHOUT_EXPLICIT_MATERIAL } from './cli.material-policy.js';
13
+ import { prepareSourceFrontierComparisonCliInput } from './cli.source-frontier-comparison.js';
13
14
 
14
15
  export async function commandInput(parsed, runtime = {}) {
15
16
  const flags = parsed.flags;
@@ -138,6 +139,8 @@ export async function commandInput(parsed, runtime = {}) {
138
139
  return { input: { plan: plan.result || plan, receipt: receipt.result || receipt, priorAcceptance: prior.result || prior }, options: {} };
139
140
  }
140
141
 
142
+ if (parsed.command === 'compare-source-frontiers') return prepareSourceFrontierComparisonCliInput(parsed, flags);
143
+
141
144
  if (parsed.command === 'manufacture-handoff-package') {
142
145
  return prepareHandoffManufactureCliCommand(parsed, runtime);
143
146
  }
@@ -1,3 +1,5 @@
1
+ import { projectPortableSourceFrontierComparisonSummary } from '../../comparison/sourceFrontierComparison.js';
2
+
1
3
  const COMMON_DEFAULT_PROJECTION = 'common-default';
2
4
 
3
5
  export function projectCommonCliDefaultOutput(result = {}, parsed = {}) {
@@ -5,9 +7,30 @@ export function projectCommonCliDefaultOutput(result = {}, parsed = {}) {
5
7
  if (parsed?.command === 'orient-handoff-package') return projectOrientDefault(result, parsed);
6
8
  if (parsed?.command === 'project-grounding-readiness') return projectGroundDefault(result, parsed);
7
9
  if (parsed?.command === 'manufacture-handoff-package' && parsed?.surfaceCommand === 'handoff') return projectHandoffDefault(result, parsed);
10
+ if (parsed?.command === 'compare-source-frontiers') return projectCompareDefault(result, parsed);
8
11
  return result;
9
12
  }
10
13
 
14
+ function projectCompareDefault(result = {}, parsed = {}) {
15
+ const summary = projectPortableSourceFrontierComparisonSummary({ ...result, schema: result.resultSchema || result.schema || '' }, { maxPaths: parsed?.flags?.['max-paths'] || 20 });
16
+ return Object.freeze({
17
+ schema: result.schema,
18
+ operation: result.operation || 'compare-source-frontiers',
19
+ resultSchema: result.resultSchema,
20
+ projection: COMMON_DEFAULT_PROJECTION,
21
+ status: summary.status,
22
+ state: summary.state,
23
+ mode: summary.mode,
24
+ inputs: summary.inputs,
25
+ workspaces: summary.workspaces,
26
+ counts: summary.counts,
27
+ findingSummary: summary.findingSummary,
28
+ actionableFindings: summary.actionableFindings,
29
+ detail: Object.freeze({ fullReceipt: Object.freeze({ command: String(parsed.surfaceCommand || 'compare'), flag: '--full' }) }),
30
+ boundary: summary.boundary
31
+ });
32
+ }
33
+
11
34
  function projectOrientDefault(result = {}, parsed = {}) {
12
35
  const projection = result?.entrypoint?.projection || {};
13
36
  const routes = (result.routes || projection.routes || []).map(projectOrientRoute);
@@ -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 packageParentWorkspaceIds = splitFlag(flags['package-parent-workspaces']);
31
32
  const packageParentWorkspaceAliases = await readOptionalJson(flags['package-parent-workspace-aliases']);
32
33
  const operatorCarrierProfile = await readOptionalJson(flags['carrier-profile']);
33
34
  const expectedToolingBootstrap = await readOptionalJson(flags['tooling-bootstrap-manifest']);
@@ -126,6 +127,7 @@ export async function prepareHandoffManufactureCliCommand(parsed = {}, runtime =
126
127
  packageParentBundle,
127
128
  packageParentPath: parentPackagePath ? path.resolve(parentPackagePath) : '',
128
129
  packageParentSha256,
130
+ packageParentWorkspaceIds,
129
131
  packageParentWorkspaceAliases
130
132
  }, runtime);
131
133
  return {
@@ -12,6 +12,7 @@ export function portableCliHelpText(commandPrefix = '', surfaceCommand = '') {
12
12
  `${command} ground <handoff-package.zip> --route <Continue-from> --holder-role <recipient-role> --continue <workspace-dir>`,
13
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
+ `${command} compare --left-kind <kind> --left <path> --right-kind <kind> --right <path> [side selectors]`,
15
16
  '',
16
17
  `Handoff aliases: ${command} orient <carrier.zip>; ${command} validate <carrier.zip>`,
17
18
  `Advanced/internal catalog: ${command} operations`,
@@ -20,7 +21,7 @@ export function portableCliHelpText(commandPrefix = '', surfaceCommand = '') {
20
21
  '- `orient`, `ground`, and public `handoff` use compact decision-first default projections; add `--full` on the same command for the complete qualified receipt.',
21
22
  '- `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
23
  '- `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. 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
+ '- `handoff` uses continuation state plus the latest qualified authored Handoff to manufacture the canonical return carrier and excludes runtime-only `.tiinex` state. A received `--package-parent` continues carrier lineage only; it never selects parent Workspace source by itself. Advanced manufacture may opt into exact parent snapshots with `--package-parent-workspaces <id,...|all>` and may bind an explicitly selected renamed predecessor 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
25
  '- Remote reads/writes remain explicit host concerns. Tooling operation safety does not create or revoke semantic Task/Handoff authority.',
25
26
  '',
26
27
  'Use `<common-command> --help` for focused common-path usage. Use `operations` deliberately for the advanced/internal operation catalog.'
@@ -28,6 +29,21 @@ export function portableCliHelpText(commandPrefix = '', surfaceCommand = '') {
28
29
  }
29
30
 
30
31
  function commonCommandHelp(command, surfaceCommand) {
32
+ if (surfaceCommand === 'compare' || surfaceCommand === 'compare-source-frontiers') return [
33
+ 'Tiinex portable tooling — compare source frontiers',
34
+ '',
35
+ 'Two-way:',
36
+ `${command} compare --left-kind <local-workspace|local-frontier|handoff-package> --left <path> --right-kind <kind> --right <path> [--left-id <workspace-id>] [--right-id <workspace-id>] [--left-select <id,...>] [--right-select <id,...>]`,
37
+ 'For local-frontier inputs use `--left-roots <id=path,...>` / `--right-roots <id=path,...>` instead of the side path.',
38
+ '',
39
+ 'Three-way child-return reconciliation:',
40
+ `${command} compare --base-kind <kind> --base <path> --incoming-kind <kind> --incoming <path> --current-kind <kind> --current <path> [side ids/selectors/roots]`,
41
+ '',
42
+ 'Input kind is mandatory and is never guessed from a path. Local source uses the exact deterministic Workspace enumeration contract shared with Handoff manufacture. Handoff packages are qualified by this current Tooling runtime; package bootstrap code is not executed. Password-sealed Workspaces remain `locked` unless a caller uses the public Node API with an already-authorized opened Workspace provider.',
43
+ 'Default output is a compact path-bounded human/LLM projection; add `--full` for the complete machine receipt. Comparison is read-only source-byte evidence only: no merge, semantic diff, staging, commit, push, remote acquisition, authority, or acceptance inference.',
44
+ '',
45
+ `Advanced/internal catalog: ${command} operations`
46
+ ];
31
47
  if (surfaceCommand === 'ground') return [
32
48
  'Tiinex portable tooling — ground',
33
49
  '',
@@ -54,7 +70,7 @@ function commonCommandHelp(command, surfaceCommand) {
54
70
  '',
55
71
  `${command} handoff <workspace-dir>`,
56
72
  '',
57
- 'Infers the latest qualified authored Handoff, selected Workspace identity/target, received package parent, unchanged sibling Workspace providers, canonical projected filename, and return output directory. The default receipt keeps output identity, routing text, closure/workspace qualification, verification, and actionable findings compact; add `--full` for the complete manufacture receipt. Normal operator completion is exactly one Handoff package plus the adjacent exact routing text. In markdown-capable hosts render that routing in a fenced code block; do not emit canonical Workspace Evidence/Handoff markdown as additional loose transport files. Runtime-only `.tiinex` state is excluded from canonical manufacture.',
73
+ 'Infers the latest qualified authored Handoff, selected Workspace identity/target, received package parent as carrier-lineage evidence, canonical projected filename, and return output directory. It does not implicitly carry sibling Workspaces from the received package; advanced manufacture must select exact reusable parent snapshots with `--package-parent-workspaces <id,...|all>`. The default receipt keeps output identity, routing text, closure/workspace qualification, verification, and actionable findings compact; add `--full` for the complete manufacture receipt. Normal operator completion is exactly one Handoff package plus the adjacent exact routing text. In markdown-capable hosts render that routing in a fenced code block; do not emit canonical Workspace Evidence/Handoff markdown as additional loose transport files. Runtime-only `.tiinex` state is excluded from canonical manufacture.',
58
74
  '',
59
75
  `Advanced/internal catalog: ${command} operations`
60
76
  ];
@@ -2,6 +2,5 @@ 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',
6
5
  'describe-cold-start-ingress','project-cold-start-host','qualify-cold-start','ground-cold-consumer'
7
6
  ]);
@@ -3,19 +3,6 @@ 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
- }
19
6
  if (command === 'project-handoff-authoring-plan') return { input: { ...material, parentPath: flags.parent || flags['parent-path'] || '', title: flags.title || '' }, options: {} };
20
7
  if (command === 'project-handoff-endpoints') return { input: { ...material, workspaceId: flags['workspace-id'] || flags.workspace || 'workspace' }, options: {} };
21
8
  if (command === 'project-operator-context') {
@@ -425,7 +425,7 @@ function withCliPhaseTiming(result = {}, timing = {}) {
425
425
  function parseArgs(argv=[]) {
426
426
  const args=[...argv],first=args.shift()||'';
427
427
  if(first==='--help'||first==='-h') return {command:'help',flags:{help:true},positionals:[]};
428
- const command=({orient:'orient-handoff-package',ground:'project-grounding-readiness',receive:'qualify-cold-start',validate:'audit-handoff-package-context',handoff:'manufacture-handoff-package',author:'author'})[first]||first;
428
+ const command=({orient:'orient-handoff-package',ground:'project-grounding-readiness',receive:'qualify-cold-start',validate:'audit-handoff-package-context',handoff:'manufacture-handoff-package',author:'author',compare:'compare-source-frontiers'})[first]||first;
429
429
  const flags={},positionals=[];
430
430
  while(args.length){const token=args.shift();if(!token.startsWith('--')){positionals.push(token);continue;}const key=token.slice(2);flags[key]=!args.length||args[0].startsWith('--')?true:args.shift();}
431
431
  return {command,flags,positionals,surfaceCommand:first};
@@ -0,0 +1,50 @@
1
+ import { prepareNodeSourceFrontierComparisonInput } from '../node/sourceFrontierComparison.js';
2
+
3
+ export async function prepareSourceFrontierComparisonCliInput(parsed = {}, flags = {}) {
4
+ const threeWay = hasAny(flags, ['base', 'base-kind', 'incoming', 'incoming-kind', 'current', 'current-kind']);
5
+ const descriptorOptions = {
6
+ maxFiles: flags['max-files'],
7
+ maxCarrierFiles: flags['max-carrier-files'],
8
+ maxTextBytes: flags['max-text-bytes']
9
+ };
10
+ if (threeWay) {
11
+ const request = {
12
+ base: descriptorFromFlags('base', flags),
13
+ incoming: descriptorFromFlags('incoming', flags),
14
+ current: descriptorFromFlags('current', flags)
15
+ };
16
+ return { input: await prepareNodeSourceFrontierComparisonInput(request, descriptorOptions), options: {} };
17
+ }
18
+ const request = {
19
+ left: descriptorFromFlags('left', flags),
20
+ right: descriptorFromFlags('right', flags)
21
+ };
22
+ return { input: await prepareNodeSourceFrontierComparisonInput(request, descriptorOptions), options: {} };
23
+ }
24
+
25
+ function descriptorFromFlags(prefix, flags) {
26
+ const kind = String(flags[`${prefix}-kind`] || '').trim();
27
+ const root = String(flags[prefix] || '').trim();
28
+ const workspaceId = String(flags[`${prefix}-id`] || flags[`${prefix}-workspace-id`] || '').trim();
29
+ const select = splitFlag(flags[`${prefix}-select`]);
30
+ const roots = parseRootBindings(flags[`${prefix}-roots`]);
31
+ return Object.freeze({
32
+ kind,
33
+ ...(root ? { path: root } : {}),
34
+ ...(workspaceId ? { workspaceId } : {}),
35
+ ...(select.length ? { workspaceIds: select } : {}),
36
+ ...(roots.length ? { workspaces: roots } : {}),
37
+ ...(flags[`${prefix}-label`] ? { label: String(flags[`${prefix}-label`]) } : {})
38
+ });
39
+ }
40
+
41
+ function parseRootBindings(value) {
42
+ const items = splitFlag(value);
43
+ return items.map((item) => {
44
+ const eq = item.indexOf('=');
45
+ if (eq <= 0 || eq === item.length - 1) return Object.freeze({ workspaceId: '', root: item });
46
+ return Object.freeze({ workspaceId: item.slice(0, eq).trim(), root: item.slice(eq + 1).trim() });
47
+ });
48
+ }
49
+ function splitFlag(value) { return !value || value === true ? [] : String(value).split(',').map((item) => item.trim()).filter(Boolean); }
50
+ function hasAny(flags, keys) { return keys.some((key) => Object.prototype.hasOwnProperty.call(flags, key)); }
@@ -15,6 +15,7 @@ import {
15
15
  } from './handoff.manufacture.requirements.js';
16
16
  import {
17
17
  expandBoundedParentBoundaryClosure,
18
+ expandRouteParentBoundaryClosure,
18
19
  normalizeWorkspaceScopes,
19
20
  normalizeWorkspaceTargetBindings,
20
21
  projectBoundedWorkspaceMaterialization
@@ -68,6 +69,7 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
68
69
  currentWorkspaceIds: [...seenWorkspaceIds],
69
70
  parentPackagePath: input.packageParentPath || '',
70
71
  parentPackageSha256: input.packageParentSha256 || '',
72
+ workspaceIds: input.packageParentWorkspaceIds || input.reusePackageParentWorkspaceIds || [],
71
73
  workspaceAliases: input.packageParentWorkspaceAliases || input.workspaceAliases || {}
72
74
  });
73
75
  const additionalEnumerationsPromise = Promise.all(additionalWorkspaceInputs.map(async ({ descriptor, id, root, requestedTitle }) => {
@@ -114,6 +116,11 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
114
116
  workspaceEnumerations.push(Object.freeze({ id, root: '', evidence: inheritedEnumeration.evidence, provider: 'qualified-package-parent-workspace' }));
115
117
  workspaceRuntimeById.set(id, Object.freeze({ id, root: '', enumeration: inheritedEnumeration, provider: 'qualified-package-parent-workspace' }));
116
118
  }
119
+ for (const provided of packageParentReuse.providers || []) {
120
+ const id = safeWorkspaceToken(provided.id || provided.enumeration?.materialization?.id || '');
121
+ if (!id || workspaceRuntimeById.has(id)) continue;
122
+ workspaceRuntimeById.set(id, Object.freeze({ id, root: '', enumeration: provided.enumeration, provider: 'qualified-package-parent-workspace-material-provider' }));
123
+ }
117
124
  const transportRoutes = Object.freeze([...(input.transportRoutes || input.handoffRoutes || [])].map((route) => normalizeTransportRoute(route, workspaceId)).filter(Boolean));
118
125
  const workspaceTargets = mergeWorkspaceTargetBindings(normalizeWorkspaceTargetBindings({
119
126
  primaryWorkspaceId: workspaceId,
@@ -137,6 +144,9 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
137
144
  const dependencyClosure = await expandPointerDependencyClosure({ requirements, materials, workspaceRuntimeById, bindings: input.materialBindings || {} });
138
145
  requirements = dependencyClosure.requirements;
139
146
  materials = dependencyClosure.materials;
147
+ const routeParentBoundaryClosure = expandRouteParentBoundaryClosure({ requirements, materials, workspaceMaterializations, workspaceRuntimeById, routeSpecs });
148
+ requirements = routeParentBoundaryClosure.requirements;
149
+ materials = routeParentBoundaryClosure.materials;
140
150
  const parentBoundaryClosure = expandBoundedParentBoundaryClosure({ requirements, materials, workspaceMaterializations, workspaceRuntimeById });
141
151
  requirements = parentBoundaryClosure.requirements;
142
152
  materials = parentBoundaryClosure.materials;
@@ -167,7 +177,11 @@ export async function prepareNodeHandoffManufacturingInput(input = {}, options =
167
177
  toolingBootstrap: toolingBootstrap.summary,
168
178
  packageParentWorkspaceReuse: Object.freeze({
169
179
  state: String(packageParentReuse.state || ''),
180
+ providerState: String(packageParentReuse.providerState || ''),
170
181
  inspectionStatus: String(packageParentReuse.inspectionStatus || ''),
182
+ selectionMode: String(packageParentReuse.selectionMode || ''),
183
+ requestedWorkspaceIds: Object.freeze([...(packageParentReuse.requestedWorkspaceIds || [])].map(String)),
184
+ providerWorkspaceIds: Object.freeze([...(packageParentReuse.providerWorkspaceIds || [])].map(String)),
171
185
  inheritedWorkspaceIds: Object.freeze((packageParentReuse.inherited || []).map((item) => String(item.id || ''))),
172
186
  workspaceAliases: Object.freeze([...(packageParentReuse.workspaceAliases || [])].map((item) => Object.freeze({ ...item }))),
173
187
  boundary: String(packageParentReuse.boundary || '')
@@ -4,48 +4,95 @@ import { parseHandoffPackageV1, RECIPIENT_V2_PACKAGE_V1_ROOT_PATH } from '../../
4
4
 
5
5
  export function preparePackageParentWorkspaceReuse(input = {}) {
6
6
  const bundle = input.bundle || null;
7
- if (!bundle?.files?.length) return emptyReuse('unavailable');
8
7
  const currentIds = new Set([...(input.currentWorkspaceIds || [])].map(normalizeId).filter(Boolean));
9
8
  const workspaceAliases = normalizePackageParentWorkspaceAliases(input.workspaceAliases || input.packageParentWorkspaceAliases || {});
10
9
  for (const alias of workspaceAliases.values()) if (!currentIds.has(alias)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-alias.target-unresolved:${alias}`);
10
+ const selection = normalizePackageParentWorkspaceSelection(input.workspaceIds || input.packageParentWorkspaceIds || input.reuseWorkspaceIds || []);
11
+ if (!bundle?.files?.length) return emptyReuse('unavailable', { selection, workspaceAliases });
11
12
  const inspection = inspectRecipientFacingV2Topology(bundle);
12
13
  const declared = declaredPackageWorkspaceBindings(bundle, inspection);
13
- if (!declared.length) return emptyReuse('unsupported-parent-surface');
14
- if (inspection.status !== 'valid') throw new Error('portable.handoff-manufacture.package-parent.workspace-provider.invalid');
15
- const missing = declared.filter((item) => !packageParentWorkspaceSupersededByCurrent(item.workspaceId, currentIds, workspaceAliases));
16
- if (!missing.length) return emptyReuse('not-needed');
14
+ if (!declared.length) {
15
+ if (selection.mode !== 'none') throw new Error('portable.handoff-manufacture.package-parent.workspace-selection.parent-surface-unresolved');
16
+ return emptyReuse('not-requested', { selection, workspaceAliases, inspectionStatus: inspection.status, providerState: 'unsupported-parent-surface' });
17
+ }
18
+ if (inspection.status !== 'valid') {
19
+ if (selection.mode !== 'none') throw new Error('portable.handoff-manufacture.package-parent.workspace-provider.invalid');
20
+ return emptyReuse('not-requested', { selection, workspaceAliases, inspectionStatus: inspection.status, providerState: 'invalid' });
21
+ }
22
+ const selected = selectDeclaredPackageParentWorkspaceBindings(declared, selection);
23
+ const missing = selected.filter((item) => !packageParentWorkspaceSupersededByCurrent(item.workspaceId, currentIds, workspaceAliases));
17
24
 
18
25
  const providerById = new Map((inspection.workspaceByteProvider?.workspaces || []).map((item) => [normalizeId(item.id), item]));
19
26
  const inspectedById = new Map((inspection.workspaces || []).map((item) => [normalizeId(item.workspaceId), item]));
20
- const inherited = [];
21
- const workspaceTargets = [];
22
- for (const binding of missing) {
27
+ const providers = [];
28
+ const providerTargetById = new Map();
29
+ for (const binding of declared) {
23
30
  const id = normalizeId(binding.workspaceId);
31
+ if (packageParentWorkspaceSupersededByCurrent(id, currentIds, workspaceAliases)) continue;
24
32
  const provider = providerById.get(id);
25
33
  const inspected = inspectedById.get(id);
26
34
  if (!provider || provider.state !== 'qualified' || provider.mode !== 'archive' || provider.materialization?.materialization !== 'complete' || inspected?.coverage !== 'complete') {
27
- throw new Error(`portable.handoff-manufacture.package-parent.workspace-provider.unqualified:${id}`);
35
+ if (selection.mode === 'all' || selection.ids.includes(id)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-provider.unqualified:${id}`);
36
+ continue;
28
37
  }
29
38
  const targetPath = String(inspected.sourceWorkspaceTargetInnerPath || binding.workspaceArtifactInnerPath || '').trim();
30
- if (!targetPath) throw new Error(`portable.handoff-manufacture.package-parent.workspace-target.unresolved:${id}`);
31
- inherited.push(buildInheritedEnumeration(provider, {
39
+ if (!targetPath) {
40
+ if (selection.mode === 'all' || selection.ids.includes(id)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-target.unresolved:${id}`);
41
+ continue;
42
+ }
43
+ providers.push(buildInheritedEnumeration(provider, {
32
44
  parentPackagePath: input.parentPackagePath || '',
33
45
  parentPackageSha256: input.parentPackageSha256 || '',
34
46
  archivePackagePath: inspected.workspaceArchivePath || binding.snapshotPath || ''
35
47
  }));
36
- workspaceTargets.push(Object.freeze({ workspaceId: id, path: targetPath, source: 'qualified-package-parent-workspace' }));
48
+ providerTargetById.set(id, targetPath);
37
49
  }
50
+ const providerEnumerationById = new Map(providers.map((item) => [normalizeId(item.id), item]));
51
+ const inherited = [];
52
+ const workspaceTargets = [];
53
+ for (const binding of missing) {
54
+ const id = normalizeId(binding.workspaceId);
55
+ const inheritedProvider = providerEnumerationById.get(id);
56
+ if (!inheritedProvider) throw new Error(`portable.handoff-manufacture.package-parent.workspace-provider.unqualified:${id}`);
57
+ inherited.push(inheritedProvider);
58
+ workspaceTargets.push(Object.freeze({ workspaceId: id, path: providerTargetById.get(id), source: 'qualified-package-parent-workspace' }));
59
+ }
60
+ const state = selection.mode === 'none' ? 'not-requested' : (missing.length ? 'qualified' : 'not-needed');
38
61
  return Object.freeze({
39
- state: 'qualified',
62
+ state,
63
+ providers: Object.freeze(providers),
64
+ providerState: 'qualified',
65
+ providerWorkspaceIds: Object.freeze(providers.map((item) => normalizeId(item.id))),
40
66
  inherited: Object.freeze(inherited),
41
67
  workspaceTargets: Object.freeze(workspaceTargets),
42
68
  inspectionStatus: inspection.status,
43
69
  missingWorkspaceIds: Object.freeze(missing.map((item) => normalizeId(item.workspaceId))),
70
+ selectionMode: selection.mode,
71
+ requestedWorkspaceIds: selection.ids,
44
72
  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.'
73
+ boundary: 'Qualified package-parent Workspace snapshots may serve as read-only exact material providers, but complete Workspace carriage is reused only for explicitly selected package-parent Workspace ids. Package-parent carrier lineage alone never selects Workspace source. 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.'
46
74
  });
47
75
  }
48
76
 
77
+ export function normalizePackageParentWorkspaceSelection(value = []) {
78
+ const raw = Array.isArray(value) ? value : [value];
79
+ const tokens = raw.flatMap((item) => String(item || '').split(',')).map((item) => String(item || '').trim()).filter(Boolean);
80
+ if (!tokens.length) return Object.freeze({ mode: 'none', ids: Object.freeze([]) });
81
+ if (tokens.some((item) => ['*', 'all'].includes(item.toLowerCase()))) return Object.freeze({ mode: 'all', ids: Object.freeze([]) });
82
+ return Object.freeze({ mode: 'explicit', ids: Object.freeze([...new Set(tokens.map(normalizeId).filter(Boolean))].sort()) });
83
+ }
84
+
85
+ export function selectDeclaredPackageParentWorkspaceBindings(declared = [], selection = normalizePackageParentWorkspaceSelection()) {
86
+ const normalized = [...(declared || [])]
87
+ .map((item) => Object.freeze({ ...item, workspaceId: normalizeId(item.workspaceId) }))
88
+ .filter((item) => item.workspaceId);
89
+ if (selection.mode === 'none') return Object.freeze([]);
90
+ if (selection.mode === 'all') return Object.freeze(normalized);
91
+ const byId = new Map(normalized.map((item) => [item.workspaceId, item]));
92
+ for (const id of selection.ids || []) if (!byId.has(id)) throw new Error(`portable.handoff-manufacture.package-parent.workspace-selection.unresolved:${id}`);
93
+ return Object.freeze((selection.ids || []).map((id) => byId.get(id)));
94
+ }
95
+
49
96
  function declaredPackageWorkspaceBindings(bundle = {}, inspection = null) {
50
97
  const roots = (bundle.files || []).filter((file) => String(file.path || '') === RECIPIENT_V2_PACKAGE_V1_ROOT_PATH);
51
98
  if (roots.length === 1) {
@@ -150,8 +197,25 @@ export function packageParentWorkspaceSupersededByCurrent(parentWorkspaceId = ''
150
197
  return Boolean(replacement && currentWorkspaceIds.has(replacement));
151
198
  }
152
199
 
153
- function emptyReuse(state) {
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.' });
200
+ function emptyReuse(state, options = {}) {
201
+ const selection = options.selection || normalizePackageParentWorkspaceSelection();
202
+ const workspaceAliases = options.workspaceAliases || new Map();
203
+ return Object.freeze({
204
+ state,
205
+ providers: Object.freeze([]),
206
+ providerState: String(options.providerState || 'unavailable'),
207
+ providerWorkspaceIds: Object.freeze([]),
208
+ inherited: Object.freeze([]),
209
+ workspaceTargets: Object.freeze([]),
210
+ inspectionStatus: String(options.inspectionStatus || ''),
211
+ missingWorkspaceIds: Object.freeze([]),
212
+ selectionMode: selection.mode,
213
+ requestedWorkspaceIds: selection.ids,
214
+ workspaceAliases: Object.freeze([...workspaceAliases.entries()].map(([parentWorkspaceId, currentWorkspaceId]) => Object.freeze({ parentWorkspaceId, currentWorkspaceId }))),
215
+ boundary: selection.mode === 'none'
216
+ ? 'Package-parent carrier lineage was supplied without an explicit package-parent Workspace reuse selection; no parent Workspace source was carried. Qualified parent Workspace providers, when available, are read-only requirement-material sources only.'
217
+ : 'No explicitly selected package-parent Workspace provider reuse was required.'
218
+ });
155
219
  }
156
220
  function normalizeId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''); }
157
221
  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'; }