@tiinex/core 0.2.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiinex/core",
3
- "version": "0.2.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": "7d910295b08a55642306e1a96d7ab913cd428099",
168
+ "gitHead": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
169
169
  "tiinexRelease": {
170
170
  "policy": "tiinex.master-npm-release.v1",
171
- "sourceCommit": "7d910295b08a55642306e1a96d7ab913cd428099",
172
- "sourceTree": "901cc01ce64d7565cfae24f832daa84962c7a22f",
171
+ "sourceCommit": "24c0ac44dcab14259b3adee85fa83434a462f9dc",
172
+ "sourceTree": "cbf5f25e875efac11c017d9914efa673bdfc2edb",
173
173
  "repository": "Tiinex/core",
174
- "previousVersion": "0.1.1"
174
+ "previousVersion": "0.2.0"
175
175
  }
176
176
  }
@@ -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') {
@@ -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.',