@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 +7 -6
- package/src/tooling/portable/adapters/cli/cli.run.js +6 -0
- package/src/tooling/portable/adapters/cli/cli.workspace-init.js +31 -0
- package/src/tooling/portable/draft/draft.create.js +7 -6
- package/src/tooling/portable/editor/editor.assistance.js +73 -16
- package/src/tooling/portable/workspace/workspace.initialize.js +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "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": "
|
|
176
|
+
"gitHead": "0115723fd777d088edda56f7d9d1bef898b359ff",
|
|
176
177
|
"tiinexRelease": {
|
|
177
178
|
"policy": "tiinex.master-npm-release.v1",
|
|
178
|
-
"sourceCommit": "
|
|
179
|
-
"sourceTree": "
|
|
179
|
+
"sourceCommit": "0115723fd777d088edda56f7d9d1bef898b359ff",
|
|
180
|
+
"sourceTree": "066320bc97b3d3d16caac487ae56a34c039bb1e2",
|
|
180
181
|
"repository": "Tiinex/core",
|
|
181
|
-
"previousVersion": "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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
139
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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; } }
|