@tiinex/core 0.21.0 → 0.23.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.21.0",
3
+ "version": "0.23.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,
@@ -140,7 +140,8 @@
140
140
  "./tooling/portable/comparison/sourceFrontierComparison.js": "./src/tooling/portable/comparison/sourceFrontierComparison.js",
141
141
  "./tooling/portable/adapters/node/sourceFrontierComparison.js": "./src/tooling/portable/adapters/node/sourceFrontierComparison.js",
142
142
  "./tooling/portable/handoff/recoveryAcceptanceAudit.js": "./src/tooling/portable/handoff/recoveryAcceptanceAudit.js",
143
- "./tooling/portable/grounding/grounding.delegationReadiness.js": "./src/tooling/portable/grounding/grounding.delegationReadiness.js"
143
+ "./tooling/portable/grounding/grounding.delegationReadiness.js": "./src/tooling/portable/grounding/grounding.delegationReadiness.js",
144
+ "./tooling/portable/workspace/workspace.initialize.js": "./src/tooling/portable/workspace/workspace.initialize.js"
144
145
  },
145
146
  "files": [
146
147
  "src",
@@ -172,12 +173,12 @@
172
173
  "type": "git",
173
174
  "url": "git+https://github.com/Tiinex/core.git"
174
175
  },
175
- "gitHead": "f27d38feae89ffc12999d288c769e5c488217412",
176
+ "gitHead": "0115723fd777d088edda56f7d9d1bef898b359ff",
176
177
  "tiinexRelease": {
177
178
  "policy": "tiinex.master-npm-release.v1",
178
- "sourceCommit": "f27d38feae89ffc12999d288c769e5c488217412",
179
- "sourceTree": "bb04bd577b9880d88648d1750a5244b7db509afe",
179
+ "sourceCommit": "0115723fd777d088edda56f7d9d1bef898b359ff",
180
+ "sourceTree": "066320bc97b3d3d16caac487ae56a34c039bb1e2",
180
181
  "repository": "Tiinex/core",
181
- "previousVersion": "0.20.0"
182
+ "previousVersion": "0.22.0"
182
183
  }
183
184
  }
@@ -9,6 +9,7 @@ import { commandInput } from './cli.command-input.js';
9
9
  import { continueGroundWithHostResult } from './cli.ground-recovery.js';
10
10
  import { groundContinuationOperationInput, materializeGroundWorkspaceCliOutput } from './cli.ground-materialize.js';
11
11
  import { runCommonAuthorCli } from './cli.common-author.js';
12
+ import { runCommonWorkspaceInitCli } from './cli.workspace-init.js';
12
13
  import { projectCommonCliDefaultOutput } from './cli.common-output.js';
13
14
 
14
15
  export async function runPortableCli(argv = process.argv.slice(2), io = console, runtime = {}) {
@@ -27,6 +28,11 @@ export async function runPortableCli(argv = process.argv.slice(2), io = console,
27
28
  writeJson(io, result, parsed.flags.compact !== true);
28
29
  return result?.findingSummary?.counts?.error ? 2 : 0;
29
30
  }
31
+ if (parsed.command === 'init-workspace') {
32
+ const result = await runCommonWorkspaceInitCli(parsed, runtime);
33
+ writeJson(io, result, parsed.flags.compact !== true);
34
+ return result?.findingSummary?.counts?.error ? 2 : 0;
35
+ }
30
36
  const timingEnabled = Boolean(parsed.flags['phase-timing']);
31
37
  const totalStartedAt = timingEnabled ? monotonicNowMs() : 0;
32
38
 
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { preparePortableWorkspaceInitialization } from '../../workspace/workspace.initialize.js';
4
+
5
+ export async function runCommonWorkspaceInitCli(parsed = {}) {
6
+ const root = path.resolve(String(parsed.positionals?.[0] || '').trim() || '.');
7
+ const flags = parsed.flags || {};
8
+ const schemaMaterialPath = String(flags['schema-material'] || '').trim();
9
+ const schemaMarkdown = schemaMaterialPath ? await readFile(path.resolve(schemaMaterialPath), 'utf8') : '';
10
+ const result = preparePortableWorkspaceInitialization({
11
+ title: String(flags.title || flags['workspace-title'] || '').trim(),
12
+ workspaceId: String(flags['workspace-id'] || '').trim(),
13
+ repository: String(flags.repository || '').trim(),
14
+ ref: String(flags.ref || '').trim(),
15
+ sourceKind: String(flags['source-kind'] || '').trim(),
16
+ rootPath: String(flags['root-path'] || '.').trim(),
17
+ authors: String(flags.authors || '').trim(),
18
+ schemaTarget: String(flags['schema-target'] || '').trim(),
19
+ schemaMarkdown,
20
+ sameRepositorySchema: flags['same-repository-schema'] === true
21
+ });
22
+ if (result.status !== 'ready') return Object.freeze({ ...result, root });
23
+ const target = path.resolve(root, ...String(result.path).split('/'));
24
+ const rel = path.relative(root, target);
25
+ if (!rel || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error('portable.workspace-init.target-outside-root');
26
+ try { await access(target); throw new Error(`portable.workspace-init.target-exists:${result.path}`); }
27
+ catch (error) { if (String(error?.message || '').startsWith('portable.workspace-init.target-exists:')) throw error; }
28
+ await mkdir(path.dirname(target), { recursive: true });
29
+ await writeFile(target, result.markdown, { encoding: 'utf8', flag: 'wx' });
30
+ return Object.freeze({ ...result, root, writeReceipt: Object.freeze({ path: target, workspaceRelativePath: result.path, bytes: Buffer.byteLength(result.markdown, 'utf8') }) });
31
+ }
@@ -24,13 +24,14 @@ export function createPortableLocalDraft(input = {}, options = {}) {
24
24
  const hasDeclaredParent = portableParentRecordHasAnyValue(parentRecord);
25
25
  const hasCompleteParent = portableParentRecordIsComplete(parentRecord);
26
26
  const exactParentQualification = qualifyPortableExactParent(parentRecord, transitionType);
27
- const localContinuityParentCompatible = exactParentQualification.state === 'qualified-local-continuity';
28
- const parentRenderable = exactParentQualification.state === 'qualified' || localContinuityParentCompatible;
29
- const qualifiedParentRecord = parentRenderable ? exactParentQualification.snapshot : null;
30
- const genericParentRecord = transitionType === 'create-artifact' ? parentRecord : qualifiedParentRecord;
31
- const exactRendererParentCompatible = exactParentQualification.state === 'qualified';
27
+ const rootCreation = transitionType === 'create-artifact';
28
+ const localContinuityParentCompatible = !rootCreation && exactParentQualification.state === 'qualified-local-continuity';
29
+ const parentRenderable = rootCreation ? !hasDeclaredParent : exactParentQualification.state === 'qualified' || localContinuityParentCompatible;
30
+ const qualifiedParentRecord = rootCreation ? null : parentRenderable ? exactParentQualification.snapshot : null;
31
+ const genericParentRecord = rootCreation ? parentRecord : qualifiedParentRecord;
32
+ const exactRendererParentCompatible = rootCreation ? !hasDeclaredParent : exactParentQualification.state === 'qualified';
32
33
  const exactRendererEligible = exactContract.status === 'ready' && exactRendererParentCompatible;
33
- const localContinuityRendererEligible = exactContract.status === 'ready' && localContinuityParentCompatible;
34
+ const localContinuityRendererEligible = !rootCreation && exactContract.status === 'ready' && localContinuityParentCompatible;
34
35
  if (!schemaId) findings.push(portableFinding('error', 'portable.draft-create.schema.required', 'Local draft creation requires a target schema id.'));
35
36
  if (hasDeclaredParent && !hasCompleteParent) findings.push(portableFinding('error', 'portable.draft-create.parent.incomplete', 'A declared Parent must provide explicit Parent Schema authority, Trace (or an explicit parent id), and a recoverable Origin path. A kind label is not Parent Schema authority.', {
36
37
  hasParentSchema: Boolean(parentRecord.schemaId || parentRecord.currentSchemaId),
@@ -4,6 +4,7 @@ import { sha256Hex } from '../../../export/package.bytes.js';
4
4
  import { integrityMethodReferenceAuthorityForCreation } from '../../../integrity/integrity.methodReference.js';
5
5
  import { inspectPortableLineageIntegrity } from '../lineage/lineage.integrity.plan.js';
6
6
  import { portableFinding } from '../findings.js';
7
+ import { qualifyTiinexRouteArtifact } from '../handoff/routeArtifactConformance.js';
7
8
 
8
9
  export const PORTABLE_EDITOR_ASSISTANCE_SCHEMA_ID = 'tiinex.portable.editor-assistance.v1';
9
10
 
@@ -28,11 +29,26 @@ function projectDocument(record = {}, records = [], lineageInspection = null) {
28
29
  const markdown = String(record.markdown || '');
29
30
  const recordPath = norm(record.path || record.id || '');
30
31
  const lineageFindings = findingsForPath(lineageInspection?.findings || [], recordPath);
31
- const sharedFindings = [...(audit.findings || []), ...lineageFindings];
32
+ const workspaceConformance = String(audit.schemaId || '') === 'tiinex.workspace.v1'
33
+ ? qualifyTiinexRouteArtifact({ markdown, expectedSchemaId: 'tiinex.workspace.v1', requireExactContract: true })
34
+ : null;
35
+ const packageQualifiedWorkspace = workspaceConformance?.status === 'qualified';
36
+ const sharedFindings = [...(audit.findings || []), ...lineageFindings].filter((finding) => !(packageQualifiedWorkspace && String(finding?.code || '') === 'audit.schema-authority.unqualified'));
32
37
  const diagnostics = sharedFindings
33
38
  .filter((item) => item.severity === 'error' || item.severity === 'warning')
34
39
  .map((finding) => projectDiagnostic(finding, markdown));
35
40
  const actions = [];
41
+ const workspacePackagingRepair = deterministicWorkspacePackagingRepair(record, audit, markdown);
42
+ if (workspacePackagingRepair.state === 'ready' && workspacePackagingRepair.markdown !== markdown) actions.push(freeze({
43
+ id: 'normalize-workspace-schema-and-self-integrity',
44
+ title: workspacePackagingRepair.schemaReferenceChanged ? 'Repair Workspace schema reference and self integrity' : 'Repair Workspace self integrity',
45
+ kind: 'replace-document',
46
+ qualification: 'deterministic-shared-core',
47
+ sourceSha256: sha256Hex(new TextEncoder().encode(markdown)),
48
+ replacementMarkdown: workspacePackagingRepair.markdown,
49
+ diagnosticCodes: workspacePackagingRepair.diagnosticCodes,
50
+ boundary: 'Repairs only a tiinex.workspace.v1 artifact whose replacement independently qualifies through the same exact registered Workspace contract and c14n-v2 self-integrity requirements used by Handoff package manufacture. Existing resolver-capable Current Schema references are preserved; permalink refresh is a separate resolution operation and must not be inferred from integrity repair.'
51
+ }));
36
52
  const integrityRepair = deterministicIntegrityHygieneRepair(markdown, audit.findings || []);
37
53
  const repairQualification = integrityRepair.state === 'ready'
38
54
  ? qualifyReplacementAgainstSharedGuardrails(record, records, integrityRepair.markdown)
@@ -54,13 +70,13 @@ function projectDocument(record = {}, records = [], lineageInspection = null) {
54
70
  path: String(record.path || record.id || ''),
55
71
  schemaId: String(audit.schemaId || ''),
56
72
  validator: {
57
- state: audit.qualification?.exact && validationAuthority?.state === 'qualified' ? 'qualified-exact' : 'degraded',
73
+ state: packageQualifiedWorkspace || (audit.qualification?.exact && validationAuthority?.state === 'qualified') ? 'qualified-exact' : 'degraded',
58
74
  requestedSchema: String(audit.qualification?.requestedSchema || audit.schemaId || ''),
59
75
  resolvedThrough: String(audit.qualification?.resolvedThrough || ''),
60
76
  fallbackUsed: Boolean(audit.qualification?.fallback?.used),
61
- authorityState: String(validationAuthority?.state || 'unavailable'),
62
- authorityBasis: String(validationAuthority?.currentReference?.basis || ''),
63
- authorityFindings: [...(validationAuthority?.findings || [])]
77
+ authorityState: packageQualifiedWorkspace ? 'qualified-package-conformance' : String(validationAuthority?.state || 'unavailable'),
78
+ authorityBasis: packageQualifiedWorkspace ? 'registered-workspace-contract+self-integrity' : String(validationAuthority?.currentReference?.basis || ''),
79
+ authorityFindings: packageQualifiedWorkspace ? [] : [...(validationAuthority?.findings || [])]
64
80
  },
65
81
  diagnostics,
66
82
  actions
@@ -72,6 +88,35 @@ function findingsForPath(findings = [], path = '') {
72
88
  return (findings || []).filter((finding) => norm(finding?.evidencePath || finding?.ref || '') === wanted);
73
89
  }
74
90
 
91
+ function deterministicWorkspacePackagingRepair(record = {}, audit = {}, markdown = '') {
92
+ if (String(audit.schemaId || '') !== 'tiinex.workspace.v1') return freeze({ state: 'unavailable' });
93
+ const source = String(markdown || '');
94
+ if (!source) return freeze({ state: 'unavailable' });
95
+ const currentMatches = [...source.matchAll(/^(\s*-\s+Current Schema:\s*)(.*)$/gm)];
96
+ if (currentMatches.length !== 1) return freeze({ state: 'unavailable' });
97
+ const currentRaw = String(currentMatches[0][2] || '').trim();
98
+ const linked = currentRaw.match(/^\[tiinex\.workspace\.v1\]\(([^)]+)\)$/);
99
+ const bare = currentRaw === 'tiinex.workspace.v1';
100
+ if (!linked && !bare) return freeze({ state: 'unavailable' });
101
+
102
+ // Current Schema is portable source identity, not a registry token. Preserve any
103
+ // already-resolvable external permalink; hosts/Core resolution may later offer a
104
+ // separate permalink refresh only when the resolved schema bytes actually changed.
105
+ let candidate = source;
106
+ const schemaReferenceChanged = false;
107
+
108
+ const sealed = sealC14nV2Self(candidate);
109
+ if (sealed.state !== 'sealed' && sealed.state !== 'unchanged') return freeze({ state: 'unavailable' });
110
+ candidate = String(sealed.markdown || candidate);
111
+ const conformance = qualifyTiinexRouteArtifact({ markdown: candidate, expectedSchemaId: 'tiinex.workspace.v1', requireExactContract: true });
112
+ if (conformance.status !== 'qualified') return freeze({ state: 'blocked', reasons: (conformance.findings || []).map((item) => String(item.code || '')) });
113
+ const diagnosticCodes = [...new Set([
114
+ ...(audit.findings || []).filter((item) => /schema-authority|integrity/i.test(String(item.code || ''))).map((item) => String(item.code || '')),
115
+ 'portable.lineage-integrity.child-self-mismatch'
116
+ ].filter(Boolean))];
117
+ return freeze({ state: 'ready', markdown: candidate, schemaReferenceChanged, diagnosticCodes });
118
+ }
119
+
75
120
  function qualifyReplacementAgainstSharedGuardrails(record = {}, records = [], replacementMarkdown = '') {
76
121
  const focusPath = norm(record.path || record.id || '');
77
122
  if (!focusPath || !replacementMarkdown) return freeze({ state: 'unavailable', reason: 'replacement-or-focus-unavailable' });
@@ -99,11 +144,23 @@ function projectDiagnostic(finding = {}, markdown = '') {
99
144
  message: String(finding.message || 'Tiinex validation finding.'),
100
145
  fixability: String(finding.fixability || 'unknown'),
101
146
  line: located.line,
147
+ sourceRange: located.sourceRange,
102
148
  locationState: located.state,
103
149
  locationBasis: located.basis
104
150
  });
105
151
  }
106
152
 
153
+ function locatedLine(lines = [], index = -1, state = 'deterministic', basis = '') {
154
+ if (!Number.isInteger(index) || index < 0 || index >= lines.length) return freeze({ state: 'unresolved', line: null, sourceRange: null, basis: basis || 'line-unavailable' });
155
+ const text = String(lines[index] || '');
156
+ return freeze({
157
+ state,
158
+ line: index + 1,
159
+ sourceRange: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: text.length + 1 },
160
+ basis
161
+ });
162
+ }
163
+
107
164
  export function locateFindingLine(finding = {}, markdown = '') {
108
165
  const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
109
166
  const params = finding.params || finding;
@@ -113,43 +170,43 @@ export function locateFindingLine(finding = {}, markdown = '') {
113
170
  const group = String(params.group || '').trim();
114
171
  if (field) {
115
172
  const index = lines.findIndex((line) => new RegExp(`^\\s*-\\s+${escapeRegExp(field)}\\s*:`).test(line));
116
- if (index >= 0) return freeze({ state: 'deterministic', line: index + 1, basis: `field:${field}` });
173
+ if (index >= 0) return locatedLine(lines, index, 'deterministic', `field:${field}`);
117
174
  }
118
175
  for (const owner of [section, heading, group].filter(Boolean)) {
119
176
  const sectionIndex = lines.findIndex((line) => new RegExp(`^#{2,6}\\s+${escapeRegExp(owner)}\\s*$`, 'i').test(line));
120
- if (sectionIndex >= 0) return freeze({ state: section || heading ? 'deterministic' : 'deterministic-anchor', line: sectionIndex + 1, basis: `${section || heading ? 'section' : 'owning-section'}:${owner}` });
177
+ if (sectionIndex >= 0) return locatedLine(lines, sectionIndex, section || heading ? 'deterministic' : 'deterministic-anchor', `${section || heading ? 'section' : 'owning-section'}:${owner}`);
121
178
  const envelopeIndex = lines.findIndex((line) => new RegExp(`^\\s*-\\s+${escapeRegExp(owner)}(?:\\s*:.*)?\\s*$`, 'i').test(line));
122
- if (envelopeIndex >= 0) return freeze({ state: 'deterministic-anchor', line: envelopeIndex + 1, basis: `envelope-owner:${owner}` });
179
+ if (envelopeIndex >= 0) return locatedLine(lines, envelopeIndex, 'deterministic-anchor', `envelope-owner:${owner}`);
123
180
  }
124
181
  const code = String(finding.code || '');
125
182
  if (code.includes('schema.') || code.endsWith('.schema.mismatch') || code === 'audit.schema-authority.unqualified') {
126
183
  const index = lines.findIndex((line) => /^\s*-\s+Current Schema\s*:/.test(line));
127
- if (index >= 0) return freeze({ state: 'deterministic', line: index + 1, basis: 'current-schema-field' });
184
+ if (index >= 0) return locatedLine(lines, index, 'deterministic', 'current-schema-field');
128
185
  }
129
186
  if (code === 'integrity.method-reference.unqualified') {
130
187
  const headingIndex = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
131
188
  const methodIndex = lines.findIndex((line, index) => index > headingIndex && /^\s*-\s+\[sha256-base64url-c14n-v2\]\([^)]+\)\s*$/.test(line));
132
- if (methodIndex >= 0) return freeze({ state: 'deterministic', line: methodIndex + 1, basis: 'continuity-integrity-method-reference' });
189
+ if (methodIndex >= 0) return locatedLine(lines, methodIndex, 'deterministic', 'continuity-integrity-method-reference');
133
190
  }
134
191
  if (code.includes('integrity') || /integrity|checksum|digest/i.test(String(finding.message || ''))) {
135
192
  const headingIndex = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
136
193
  if (headingIndex >= 0) {
137
194
  const index = lines.findIndex((line, i) => i > headingIndex && /^\s+-\s+Value\s*:/.test(line));
138
- if (index >= 0) return freeze({ state: 'deterministic', line: index + 1, basis: 'continuity-integrity-value' });
139
- return freeze({ state: 'deterministic-anchor', line: headingIndex + 1, basis: 'continuity-integrity-heading' });
195
+ if (index >= 0) return locatedLine(lines, index, 'deterministic', 'continuity-integrity-value');
196
+ return locatedLine(lines, headingIndex, 'deterministic-anchor', 'continuity-integrity-heading');
140
197
  }
141
198
  }
142
199
  if (/^(portable\.contract\.|root\.|integrity\.)/i.test(code) && (/missing|required|incomplete/i.test(code) || /\bmissing\b|\brequired\b/i.test(String(finding.message || '')))) {
143
200
  const bodyHeading = lines.findIndex((line) => /^#\s+\S/.test(line) && !/^#\s+Continuity (?:Context|Integrity)\s*$/.test(line));
144
- if (bodyHeading >= 0) return freeze({ state: 'deterministic-anchor', line: bodyHeading + 1, basis: section ? `body-heading-for-missing-section:${section}` : field ? `body-heading-for-missing-field:${field}` : 'body-heading-for-missing-required-content' });
201
+ if (bodyHeading >= 0) return locatedLine(lines, bodyHeading, 'deterministic-anchor', section ? `body-heading-for-missing-section:${section}` : field ? `body-heading-for-missing-field:${field}` : 'body-heading-for-missing-required-content');
145
202
  const contextHeading = lines.findIndex((line) => line.trim() === '# Continuity Context');
146
- if (contextHeading >= 0) return freeze({ state: 'deterministic-anchor', line: contextHeading + 1, basis: 'continuity-context-for-missing-required-content' });
203
+ if (contextHeading >= 0) return locatedLine(lines, contextHeading, 'deterministic-anchor', 'continuity-context-for-missing-required-content');
147
204
  }
148
205
  if (/\.body\.|body/i.test(code) || /\bbody\b/i.test(String(finding.message || ''))) {
149
206
  const bodyHeading = lines.findIndex((line) => /^#\s+\S/.test(line) && !/^#\s+Continuity (?:Context|Integrity)\s*$/.test(line));
150
- if (bodyHeading >= 0) return freeze({ state: 'deterministic-anchor', line: bodyHeading + 1, basis: 'body-heading-for-body-finding' });
207
+ if (bodyHeading >= 0) return locatedLine(lines, bodyHeading, 'deterministic-anchor', 'body-heading-for-body-finding');
151
208
  }
152
- return freeze({ state: 'unresolved', line: null, basis: 'shared-finding-has-no-deterministic-line-evidence' });
209
+ return freeze({ state: 'unresolved', line: null, sourceRange: null, basis: 'shared-finding-has-no-deterministic-line-evidence' });
153
210
  }
154
211
 
155
212
  function deterministicIntegrityHygieneRepair(markdown = '', findings = []) {
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { parseArtifactMarkdown } from '../../../artifacts/artifact.parse.js';
3
+ import { sealC14nV2Self, canonicalC14nV2SelfState } from '../../../integrity/integrity.c14nV2.js';
4
+ import { integrityMethodReferenceAuthorityForCreation, C14N_V2_METHOD_ID } from '../../../integrity/integrity.methodReference.js';
5
+ import { schemaReferenceAuthorityForRegisteredSchema } from '../../../schemas/creation.schemaReferences.js';
6
+ import { compilePortableSchemaContract } from '../schema/contract.compile.js';
7
+ import { workspaceValidate } from '../../../schemas/workspace/tiinex.workspace.v1.validate.js';
8
+
9
+ export const PORTABLE_WORKSPACE_INITIALIZATION_SCHEMA_ID = 'tiinex.portable.workspace-initialization.v1';
10
+ export const WORKSPACE_SCHEMA_RESOLUTION_REQUEST = Object.freeze({
11
+ schemaId: 'tiinex.workspace.v1',
12
+ repository: 'Tiinex/docs',
13
+ path: '.topics/.schemas/tiinex.workspace.v1.schema.md',
14
+ resolution: 'latest-commit-permalink-plus-exact-schema-bytes',
15
+ boundary: 'The host resolves bytes; Core validates schema identity and owns rendered Workspace semantics. A commit change alone is not schema-change evidence.'
16
+ });
17
+
18
+ export function preparePortableWorkspaceInitialization(input = {}) {
19
+ const findings = [];
20
+ const title = clean(input.title || input.workspaceId || repositoryLeaf(input.repository) || 'Workspace');
21
+ const workspaceId = slug(input.workspaceId || repositoryLeaf(input.repository) || title);
22
+ const schemaTarget = String(input.schemaTarget || input.schemaResolution?.target || '').trim();
23
+ const schemaMarkdown = String(input.schemaMarkdown || input.schemaResolution?.markdown || '');
24
+ if (!schemaTarget || !schemaMarkdown) return Object.freeze({
25
+ schema: PORTABLE_WORKSPACE_INITIALIZATION_SCHEMA_ID,
26
+ status: 'needs-resolution',
27
+ resolutionRequest: WORKSPACE_SCHEMA_RESOLUTION_REQUEST,
28
+ findingSummary: summary([]),
29
+ findings: Object.freeze([])
30
+ });
31
+ if (!isResolvableSchemaTarget(schemaTarget, input.sameRepositorySchema === true)) findings.push(finding('error', 'workspace.initialize.schema-target.unresolvable', 'Workspace Current Schema must remain resolvable: use a same-repository relative target or an absolute permalink.', { schemaTarget }));
32
+ let compiled = null;
33
+ try { compiled = compilePortableSchemaContract(schemaMarkdown); }
34
+ catch (error) { findings.push(finding('error', 'workspace.initialize.schema-material.invalid', 'Resolved Workspace schema material could not be compiled.', { detail: String(error?.message || error || '') })); }
35
+ if (compiled && String(compiled.schemaId || '') !== 'tiinex.workspace.v1') findings.push(finding('error', 'workspace.initialize.schema-id.mismatch', 'Resolved schema material is not tiinex.workspace.v1.', { observed: compiled.schemaId || '' }));
36
+
37
+ const repository = clean(input.repository || '');
38
+ const ref = clean(input.ref || '');
39
+ const sourceKind = clean(input.sourceKind || (repository ? 'github-tree' : 'local-session'));
40
+ const rootPath = clean(input.rootPath || '.');
41
+ const createdAt = timestamp(input.createdAt || new Date());
42
+ const authors = clean(input.authors || 'local-user');
43
+ const envelopeTarget = String(schemaReferenceAuthorityForRegisteredSchema('tiinex.root.v1')?.preferredTarget || '').trim();
44
+ const integrityTarget = String(integrityMethodReferenceAuthorityForCreation(C14N_V2_METHOD_ID)?.preferredTarget || '').trim();
45
+ if (!envelopeTarget) findings.push(finding('error', 'workspace.initialize.root-schema.unresolved', 'Core could not resolve canonical tiinex.root.v1 authority.'));
46
+ if (!integrityTarget) findings.push(finding('error', 'workspace.initialize.integrity-method.unresolved', 'Core could not resolve canonical c14n-v2 validator authority.'));
47
+ if (findings.some((item) => item.severity === 'error')) return blocked(findings);
48
+
49
+ const entrypoint = repository ? `\n## Workspace Entrypoints\n\n### Repository source\n\n- Source Kind: ${sourceKind}\n- Repository: ${repository}${ref ? `\n- Ref: ${ref}` : ''}\n- Root Path: ${rootPath}\n- Repo Files Discovery: on\n` : '';
50
+ const unsigned = `# Continuity Context\n\n- Envelope Schema: [tiinex.root.v1](${envelopeTarget})\n- Current\n - Current Schema: [tiinex.workspace.v1](${schemaTarget})\n - Created At: ${createdAt}\n - Authors: ${authors}\n - Why: Establish an explicit portable Workspace entrypoint for this repository.\n - Summary: ${title} Workspace.\n - Status: active/local\n\n---\n\n# ${title}${entrypoint}\n# Continuity Integrity\n\n- [sha256-base64url-c14n-v2](${integrityTarget})\n - Towards: self\n - Value: pending\n`;
51
+ const sealed = sealC14nV2Self(unsigned);
52
+ if (sealed.state !== 'sealed') return blocked([finding('error', 'workspace.initialize.integrity-seal.failed', 'Core could not seal Workspace c14n-v2 self integrity.', { reason: sealed.reason || sealed.state })]);
53
+ const parsed = parseArtifactMarkdown(sealed.markdown);
54
+ for (const item of workspaceValidate(parsed) || []) if (item?.severity === 'error') findings.push(finding('error', item.code || 'workspace.initialize.workspace-validation', item.message || 'Workspace validation failed.'));
55
+ const integrity = canonicalC14nV2SelfState(sealed.markdown);
56
+ if (integrity.state !== 'verified') findings.push(finding('error', 'workspace.initialize.integrity-verification.failed', 'Generated Workspace self integrity did not verify.', { reason: integrity.reason || integrity.state }));
57
+ if (String(parsed?.envelope?.current?.schema?.id || '') !== 'tiinex.workspace.v1') findings.push(finding('error', 'workspace.initialize.current-schema.mismatch', 'Generated Workspace Current Schema identity is not tiinex.workspace.v1.'));
58
+ if (findings.some((item) => item.severity === 'error')) return blocked(findings);
59
+ return Object.freeze({
60
+ schema: PORTABLE_WORKSPACE_INITIALIZATION_SCHEMA_ID,
61
+ status: 'ready',
62
+ workspaceId,
63
+ path: `.topics/.workspaces/${workspaceId}.workspace.md`,
64
+ markdown: sealed.markdown,
65
+ schemaReference: Object.freeze({ schemaId: 'tiinex.workspace.v1', target: schemaTarget }),
66
+ repository: Object.freeze({ repository, ref, sourceKind, rootPath }),
67
+ findingSummary: summary(findings),
68
+ findings: Object.freeze(findings),
69
+ boundary: 'Core owns Workspace artifact semantics, schema-reference preservation, and integrity. Hosts only resolve requested external schema bytes and perform the explicit filesystem write.'
70
+ });
71
+ }
72
+
73
+ function blocked(findings) { return Object.freeze({ schema: PORTABLE_WORKSPACE_INITIALIZATION_SCHEMA_ID, status: 'blocked', resolutionRequest: WORKSPACE_SCHEMA_RESOLUTION_REQUEST, findingSummary: summary(findings), findings: Object.freeze(findings) }); }
74
+ function finding(severity, code, message, evidence = {}) { return Object.freeze({ severity, code, message, source: PORTABLE_WORKSPACE_INITIALIZATION_SCHEMA_ID, evidence: Object.freeze({ ...evidence }) }); }
75
+ function summary(findings) { const counts={error:0,warning:0,info:0,total:findings.length}; for(const item of findings) if(counts[item.severity]!==undefined) counts[item.severity]+=1; return Object.freeze({status:counts.error?'blocked':counts.warning?'degraded':'clean',counts:Object.freeze(counts)}); }
76
+ function clean(value) { return String(value ?? '').trim().replace(/[\r\n]+/g, ' '); }
77
+ function slug(value) { const out=clean(value).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''); return out || 'workspace'; }
78
+ function repositoryLeaf(value) { const text=clean(value).replace(/\.git$/i,'').replace(/[\\/]+$/,''); return text.split(/[\\/]/).filter(Boolean).at(-1) || ''; }
79
+ function timestamp(value) { const date=value instanceof Date?value:new Date(value); if(Number.isNaN(date.getTime())) return clean(value); return date.toISOString().replace('T',' ').replace(/\.\d{3}Z$/,''); }
80
+ function isResolvableSchemaTarget(value, sameRepository) { if(sameRepository && !/^[a-z][a-z0-9+.-]*:/i.test(value) && !value.startsWith('/')) return true; try { const url=new URL(value); return ['https:','http:'].includes(url.protocol); } catch { return false; } }