@tiinex/core 0.32.0 → 0.34.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 +5 -5
- package/src/schemas/creation.contracts.js +92 -10
- package/src/schemas/creation.renderer.js +10 -3
- package/src/tooling/portable/adapters/cli/cli.editor-assistance.js +5 -2
- package/src/tooling/portable/draft/draft.exact.js +1 -1
- package/src/tooling/portable/editor/editor.assistance.js +228 -11
- package/src/tooling/portable/lineage/lineage.integrity.plan.js +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiinex/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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,
|
|
@@ -173,12 +173,12 @@
|
|
|
173
173
|
"type": "git",
|
|
174
174
|
"url": "git+https://github.com/Tiinex/core.git"
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "873ac202d2f12a6fbf43437c61b198a85dd7afb3",
|
|
177
177
|
"tiinexRelease": {
|
|
178
178
|
"policy": "tiinex.master-npm-release.v1",
|
|
179
|
-
"sourceCommit": "
|
|
180
|
-
"sourceTree": "
|
|
179
|
+
"sourceCommit": "873ac202d2f12a6fbf43437c61b198a85dd7afb3",
|
|
180
|
+
"sourceTree": "90fab47ce492c14411836043f221511212b5471e",
|
|
181
181
|
"repository": "Tiinex/core",
|
|
182
|
-
"previousVersion": "0.
|
|
182
|
+
"previousVersion": "0.33.0"
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -10,6 +10,7 @@ import { projectPortableValidationContractWithQualifiedLocalRoot } from '../tool
|
|
|
10
10
|
import { qualifyRootCreationRepresentation, qualifyContinuationCreationRepresentation } from './creation.representation.js';
|
|
11
11
|
import { qualifyCreationSchemaReferences, schemaReferenceAuthoritiesForCreation } from './creation.schemaReferences.js';
|
|
12
12
|
import { qualifySchemaReferenceValue } from './schema.reference.js';
|
|
13
|
+
import { qualifiedCreationAuthorityFromSchemaSource } from './schema.source.js';
|
|
13
14
|
import { C14N_V2_METHOD_ID, integrityMethodReferenceAuthorityForCreation } from '../integrity/integrity.methodReference.js';
|
|
14
15
|
|
|
15
16
|
export const ARTIFACT_CREATION_CONTRACT_SCHEMA_ID = 'tiinex.artifact.creation.contract.v1';
|
|
@@ -39,16 +40,7 @@ export function buildArtifactCreationContract(input = {}, options = {}) {
|
|
|
39
40
|
const transitionType = String(input.transitionType || options.transitionType || 'create-artifact').trim();
|
|
40
41
|
const creationCapability = qualifyArtifactCreationCapability(module, transitionType);
|
|
41
42
|
const creationAuthority = creationCapability.authority?.compiledContract?.creation || {};
|
|
42
|
-
const creation =
|
|
43
|
-
requiredInputs: list(creationAuthority.requiredInputs),
|
|
44
|
-
optionalInputs: list(creationAuthority.optionalInputs),
|
|
45
|
-
requiredSections: list(creationAuthority.requiredSections),
|
|
46
|
-
representationSections: list(creationAuthority.representationSections),
|
|
47
|
-
toolingConfigurationFields: list(creationAuthority.toolingConfigurationFields),
|
|
48
|
-
inputBindings: list(creationAuthority.inputBindings),
|
|
49
|
-
supplementalRequiredFields: list(creationAuthority.supplementalRequiredFields),
|
|
50
|
-
requiredShape: list(creationAuthority.requiredShape)
|
|
51
|
-
});
|
|
43
|
+
const creation = composeCreationAuthority(module, creationCapability.authority, creationAuthority);
|
|
52
44
|
const renderer = creationCapability.implementation?.state === 'implemented'
|
|
53
45
|
? { status: CapabilityStatus.implemented, ...(creationCapability.implementation.renderer || {}) }
|
|
54
46
|
: { status: CapabilityStatus.unavailable, id: '', scope: transitionType };
|
|
@@ -203,6 +195,96 @@ export function validateArtifactCreationResult(draft = {}, parentRecord = {}, op
|
|
|
203
195
|
|
|
204
196
|
|
|
205
197
|
|
|
198
|
+
|
|
199
|
+
function composeCreationAuthority(module = null, authority = {}, localCreation = {}) {
|
|
200
|
+
const validationContract = authority?.compiledContract?.validationContract || null;
|
|
201
|
+
const lineage = Array.isArray(validationContract?.lineage) && validationContract.lineage.length
|
|
202
|
+
? validationContract.lineage
|
|
203
|
+
: [String(module?.id || authority?.schemaId || '')].filter(Boolean);
|
|
204
|
+
const creationAuthorities = [];
|
|
205
|
+
for (const schemaId of lineage) {
|
|
206
|
+
const lineageModule = schemaRegistry.byId?.get(schemaId) || null;
|
|
207
|
+
if (!lineageModule) continue;
|
|
208
|
+
const qualified = qualifiedCreationAuthorityFromSchemaSource(lineageModule);
|
|
209
|
+
if (qualified.state !== 'qualified' || !qualified.compiledContract?.creation) continue;
|
|
210
|
+
creationAuthorities.push({ schemaId, creation: qualified.compiledContract.creation });
|
|
211
|
+
}
|
|
212
|
+
if (!creationAuthorities.length) creationAuthorities.push({ schemaId: String(module?.id || ''), creation: localCreation });
|
|
213
|
+
|
|
214
|
+
const requiredHeadingOrder = [...(validationContract?.validation?.requiredHeadings || [])]
|
|
215
|
+
.filter((item) => Number(item?.level || 0) === 2 && String(item?.title || '').trim())
|
|
216
|
+
.map((item) => String(item.title).trim());
|
|
217
|
+
const requiredHeadingSet = new Set(requiredHeadingOrder);
|
|
218
|
+
const bindingByInput = new Map();
|
|
219
|
+
const requiredInputSet = new Set();
|
|
220
|
+
const optionalInputSet = new Set();
|
|
221
|
+
const toolingFields = [];
|
|
222
|
+
const supplementalByKey = new Map();
|
|
223
|
+
const shapeByKey = new Map();
|
|
224
|
+
|
|
225
|
+
for (const entry of creationAuthorities) {
|
|
226
|
+
const creation = entry.creation || {};
|
|
227
|
+
for (const value of creation.requiredInputs || []) requiredInputSet.add(String(value || '').trim());
|
|
228
|
+
for (const value of creation.optionalInputs || []) optionalInputSet.add(String(value || '').trim());
|
|
229
|
+
for (const value of creation.toolingConfigurationFields || []) pushUnique(toolingFields, String(value || '').trim());
|
|
230
|
+
for (const item of creation.inputBindings || []) {
|
|
231
|
+
const input = String(item?.input || '').trim();
|
|
232
|
+
if (!input) continue;
|
|
233
|
+
const section = String(item?.section || '').trim();
|
|
234
|
+
if (section && requiredHeadingSet.size && !requiredHeadingSet.has(section)) continue;
|
|
235
|
+
bindingByInput.set(input, item);
|
|
236
|
+
}
|
|
237
|
+
for (const item of creation.supplementalRequiredFields || []) {
|
|
238
|
+
const section = String(item?.section || '').trim();
|
|
239
|
+
if (section && requiredHeadingSet.size && !requiredHeadingSet.has(section)) continue;
|
|
240
|
+
const key = `${section}\u0000${String(item?.field || '')}`;
|
|
241
|
+
supplementalByKey.set(key, item);
|
|
242
|
+
}
|
|
243
|
+
for (const item of creation.requiredShape || []) {
|
|
244
|
+
const primitive = item?.primitive || {};
|
|
245
|
+
const section = String(primitive?.section || '').trim();
|
|
246
|
+
if (section && requiredHeadingSet.size && !requiredHeadingSet.has(section)) continue;
|
|
247
|
+
const key = `${String(primitive?.kind || '')}\u0000${String(primitive?.input || '')}\u0000${section}\u0000${String(primitive?.position || '')}`;
|
|
248
|
+
shapeByKey.set(key, item);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const bindings = [];
|
|
253
|
+
for (const item of bindingByInput.values()) if (!String(item?.section || '').trim()) bindings.push(item);
|
|
254
|
+
for (const section of requiredHeadingOrder) {
|
|
255
|
+
for (const item of bindingByInput.values()) if (String(item?.section || '').trim() === section && !bindings.includes(item)) bindings.push(item);
|
|
256
|
+
}
|
|
257
|
+
for (const item of bindingByInput.values()) if (!bindings.includes(item)) bindings.push(item);
|
|
258
|
+
|
|
259
|
+
const boundInputs = new Set(bindings.map((item) => String(item?.input || '').trim()).filter(Boolean));
|
|
260
|
+
const requiredInputs = [...requiredInputSet].filter((input) => input && boundInputs.has(input));
|
|
261
|
+
const optionalInputs = [...optionalInputSet].filter((input) => input && boundInputs.has(input) && !requiredInputSet.has(input));
|
|
262
|
+
const requiredSections = requiredHeadingOrder.length
|
|
263
|
+
? requiredHeadingOrder.filter((section) => bindings.some((item) => String(item?.section || '').trim() === section))
|
|
264
|
+
: uniqueStrings(creationAuthorities.flatMap((entry) => entry.creation?.requiredSections || []));
|
|
265
|
+
const representationSections = requiredSections.length
|
|
266
|
+
? requiredSections
|
|
267
|
+
: uniqueStrings(bindings.map((item) => String(item?.section || '').trim()).filter(Boolean));
|
|
268
|
+
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
requiredInputs: Object.freeze(requiredInputs),
|
|
271
|
+
optionalInputs: Object.freeze(optionalInputs),
|
|
272
|
+
requiredSections: Object.freeze(requiredSections),
|
|
273
|
+
representationSections: Object.freeze(representationSections),
|
|
274
|
+
toolingConfigurationFields: Object.freeze(toolingFields),
|
|
275
|
+
inputBindings: Object.freeze(bindings),
|
|
276
|
+
supplementalRequiredFields: Object.freeze([...supplementalByKey.values()]),
|
|
277
|
+
requiredShape: Object.freeze([...shapeByKey.values()])
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function uniqueStrings(values = []) {
|
|
282
|
+
const out = [];
|
|
283
|
+
for (const value of values || []) pushUnique(out, String(value || '').trim());
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
function pushUnique(values, value) { if (value && !values.includes(value)) values.push(value); }
|
|
287
|
+
|
|
206
288
|
function validateParentTargetIntegrity(parsed = {}, parentRecord = {}, expectedTarget = '') {
|
|
207
289
|
const entries = (parsed?.integrity?.entries || []).filter((entry) => entry?.method === C14N_V2_METHOD_ID && String(entry?.towards || '') !== 'self');
|
|
208
290
|
if (entries.length !== 1) return [];
|
|
@@ -38,7 +38,7 @@ export function renderArtifactCreationDraftMarkdown(contract = {}, input = {}) {
|
|
|
38
38
|
const lines = [
|
|
39
39
|
'# Continuity Context', '',
|
|
40
40
|
`- Envelope Schema: ${envelopeSchemaReference}`,
|
|
41
|
-
...(parent ? renderParent(parent) : []),
|
|
41
|
+
...(parent ? renderParent(parent, { preserveUnqualifiedSchemaReference: input.preserveUnqualifiedParentSchemaReference === true }) : []),
|
|
42
42
|
'- Current',
|
|
43
43
|
` - Current Schema: ${currentSchemaReference}`,
|
|
44
44
|
` - Created At: ${createdAt}`,
|
|
@@ -69,10 +69,10 @@ export const genericArtifactCreationImplementation = Object.freeze({
|
|
|
69
69
|
execute: renderArtifactCreationDraftMarkdown
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
function renderParent(parent) {
|
|
72
|
+
function renderParent(parent, options = {}) {
|
|
73
73
|
return [
|
|
74
74
|
'- Parent',
|
|
75
|
-
` - Parent Schema: ${
|
|
75
|
+
` - Parent Schema: ${renderParentSchemaReference(parent.schemaReferenceAuthority, options.preserveUnqualifiedSchemaReference === true)}`,
|
|
76
76
|
...(parent.createdAt ? [` - Created At: ${parent.createdAt}`] : []),
|
|
77
77
|
` - Trace: [${parent.traceLabel}](${parent.traceReference})`,
|
|
78
78
|
' - Origin:',
|
|
@@ -81,6 +81,13 @@ function renderParent(parent) {
|
|
|
81
81
|
];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function renderParentSchemaReference(authority = {}, preserveUnqualified = false) {
|
|
85
|
+
const schemaId = String(authority?.schemaId || '').trim();
|
|
86
|
+
const preferredTarget = String(authority?.preferredTarget || authority?.target || '').trim();
|
|
87
|
+
if (preserveUnqualified && schemaId && preferredTarget) return `[${schemaId}](${preferredTarget})`;
|
|
88
|
+
return renderSchemaReference(authority);
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
function contractDrivenBodyMarkdown(contract = {}, { title = '', values = {} } = {}) {
|
|
85
92
|
const creation = contract?.creation || {};
|
|
86
93
|
const bindings = Array.isArray(creation.inputBindings) ? creation.inputBindings : [];
|
|
@@ -2,10 +2,13 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
|
|
3
3
|
export async function prepareEditorAssistanceCliInput(material = {}, flags = {}) {
|
|
4
4
|
const focusPath = norm(flags.focus || flags.path || '');
|
|
5
|
-
|
|
5
|
+
const referenceResolutions = flags['reference-resolutions']
|
|
6
|
+
? JSON.parse(await readFile(String(flags['reference-resolutions']), 'utf8'))
|
|
7
|
+
: [];
|
|
8
|
+
if (!focusPath || !flags.overlay) return { input: { ...material, focusPath, referenceResolutions }, options: {} };
|
|
6
9
|
const overlayMarkdown = await readFile(String(flags.overlay), 'utf8');
|
|
7
10
|
const files = (material.files || []).map((file) => norm(file.path || '') === focusPath ? { ...file, content: overlayMarkdown } : file);
|
|
8
|
-
const input = { ...material, files, focusPath };
|
|
11
|
+
const input = { ...material, files, focusPath, referenceResolutions };
|
|
9
12
|
if (Array.isArray(material.records)) input.records = material.records.map((record) => norm(record.path || record.id || '') === focusPath ? { ...record, markdown: overlayMarkdown } : record);
|
|
10
13
|
return { input, options: {} };
|
|
11
14
|
}
|
|
@@ -78,7 +78,7 @@ export function renderQualifiedPortableExactDraft({ contract = {}, schemaId = ''
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function renderPortableLocalContinuityDraft({ contract = {}, schemaId = '', transitionType = 'continue-from-record', parentSnapshot = {}, rendererInput = {} } = {}) {
|
|
81
|
-
const markdown = renderArtifactCreationCandidateMarkdown(contract, { ...rendererInput, parentRecord: parentSnapshot, currentSchemaId: schemaId });
|
|
81
|
+
const markdown = renderArtifactCreationCandidateMarkdown(contract, { ...rendererInput, parentRecord: parentSnapshot, currentSchemaId: schemaId, preserveUnqualifiedParentSchemaReference: true });
|
|
82
82
|
if (!markdown) return Object.freeze({ state: 'unqualified', reason: 'local-continuity-renderer-empty', markdown: '', validation: null, parentRepresentation: null });
|
|
83
83
|
const parentRepresentation = qualifyPortableRenderedParentRepresentation(markdown, parentSnapshot, transitionType, rendererInput.childPath || '', { allowUnpublishedLocalParent: true });
|
|
84
84
|
if (parentRepresentation.state !== 'qualified') return Object.freeze({ state: 'unqualified', reason: parentRepresentation.reason || 'local-continuity-parent-representation-mismatch', markdown: '', validation: null, parentRepresentation });
|
|
@@ -14,7 +14,7 @@ export function projectPortableEditorAssistance(input = {}) {
|
|
|
14
14
|
const focusPath = norm(input.focusPath || input.focus || '');
|
|
15
15
|
const selectedRecords = focusPath ? records.filter((record) => norm(record.path || record.id || '') === focusPath) : records;
|
|
16
16
|
const lineageInspection = inspectPortableLineageIntegrity({ records });
|
|
17
|
-
const documents = selectedRecords.map((record) => projectDocument(record, records, lineageInspection));
|
|
17
|
+
const documents = selectedRecords.map((record) => projectDocument(record, records, lineageInspection, referenceResolutionsForRecord(input.referenceResolutions || [], record)));
|
|
18
18
|
const diagnostics = documents.flatMap((item) => item.diagnostics);
|
|
19
19
|
return freeze({
|
|
20
20
|
schema: PORTABLE_EDITOR_ASSISTANCE_SCHEMA_ID,
|
|
@@ -25,7 +25,7 @@ export function projectPortableEditorAssistance(input = {}) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function projectDocument(record = {}, records = [], lineageInspection = null) {
|
|
28
|
+
function projectDocument(record = {}, records = [], lineageInspection = null, referenceResolutions = []) {
|
|
29
29
|
const audit = auditPortableRecord(record, { requireExactSchemaAuthority: true });
|
|
30
30
|
const markdown = String(record.markdown || '');
|
|
31
31
|
const recordPath = norm(record.path || record.id || '');
|
|
@@ -72,9 +72,33 @@ function projectDocument(record = {}, records = [], lineageInspection = null) {
|
|
|
72
72
|
boundary: 'Repairs only deterministically malformed Workspace-qualified Parent recovery locators and/or a bare Current Schema id when exact qualified current-schema source authority exists; reseals self integrity and exposes the replacement only after shared audit and loaded-descendant guardrails re-qualify it. External Parent availability is not invented.'
|
|
73
73
|
}));
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const referenceResolution = projectReferenceResolutionAssistance(markdown, referenceResolutions);
|
|
76
|
+
diagnostics.push(...referenceResolution.diagnostics);
|
|
77
|
+
actions.push(...referenceResolution.actions);
|
|
78
|
+
|
|
79
|
+
const lineageArtifact = (lineageInspection?.artifacts || []).find((item) => norm(item?.path || '') === recordPath) || null;
|
|
80
|
+
const parentIntegrityRepair = deterministicParentIntegrityRepair(markdown, lineageArtifact);
|
|
81
|
+
const parentRepairQualification = parentIntegrityRepair.state === 'ready'
|
|
82
|
+
? qualifyReplacementAgainstSharedGuardrails(record, records, parentIntegrityRepair.markdown, {
|
|
83
|
+
allowExistingWarningCodes: sharedFindings.filter((item) => item.severity === 'warning').map((item) => String(item.code || ''))
|
|
84
|
+
})
|
|
85
|
+
: { state: 'unavailable' };
|
|
86
|
+
if (parentIntegrityRepair.state === 'ready' && parentIntegrityRepair.markdown !== markdown && parentRepairQualification.state === 'qualified') actions.push(freeze({
|
|
87
|
+
id: 'refresh-parent-integrity-and-self-seal',
|
|
88
|
+
title: 'Refresh Tiinex Parent integrity and self seal',
|
|
89
|
+
kind: 'replace-document',
|
|
90
|
+
qualification: 'deterministic-shared-core',
|
|
91
|
+
sourceSha256: sha256Hex(new TextEncoder().encode(markdown)),
|
|
92
|
+
replacementMarkdown: parentIntegrityRepair.markdown,
|
|
93
|
+
diagnosticCodes: ['portable.lineage-integrity.parent-target-mismatch', 'integrity.c14n-v2.mismatch', 'portable.lineage-integrity.child-self-mismatch'],
|
|
94
|
+
boundary: 'Refreshes only an existing Parent integrity digest whose declared locator already matches the exact resolved Parent and whose verified Parent self digest is available locally; the focused artifact is resealed and the action is withheld when loaded descendants would become inconsistent.'
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
const integrityRepair = deterministicIntegrityHygieneRepair(markdown, sharedFindings);
|
|
76
98
|
const repairQualification = integrityRepair.state === 'ready'
|
|
77
|
-
? qualifyReplacementAgainstSharedGuardrails(record, records, integrityRepair.markdown
|
|
99
|
+
? qualifyReplacementAgainstSharedGuardrails(record, records, integrityRepair.markdown, {
|
|
100
|
+
allowExistingWarningCodes: sharedFindings.filter((item) => item.severity === 'warning').map((item) => String(item.code || ''))
|
|
101
|
+
})
|
|
78
102
|
: { state: 'unavailable' };
|
|
79
103
|
if (integrityRepair.state === 'ready' && integrityRepair.markdown !== markdown && repairQualification.state === 'qualified') actions.push(freeze({
|
|
80
104
|
id: 'refresh-primary-self-integrity',
|
|
@@ -232,6 +256,110 @@ function deterministicReferenceHygieneRepair(record = {}, audit = {}, markdown =
|
|
|
232
256
|
return freeze({ state: 'ready', markdown: String(sealed.markdown || candidate), parentReferenceChanged, schemaReferenceChanged, diagnosticCodes: [...new Set(diagnosticCodes)] });
|
|
233
257
|
}
|
|
234
258
|
|
|
259
|
+
function referenceResolutionsForRecord(resolutions = [], record = {}) {
|
|
260
|
+
const recordPath = norm(record.path || record.id || '');
|
|
261
|
+
return (Array.isArray(resolutions) ? resolutions : []).filter((item) => {
|
|
262
|
+
const itemPath = norm(item?.path || '');
|
|
263
|
+
return !itemPath || itemPath === recordPath;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function projectReferenceResolutionAssistance(markdown = '', resolutions = []) {
|
|
268
|
+
const source = String(markdown || '');
|
|
269
|
+
if (!source || !Array.isArray(resolutions) || !resolutions.length) return freeze({ diagnostics: [], actions: [] });
|
|
270
|
+
const byTarget = new Map(resolutions.map((item) => [String(item?.target || '').trim(), item]).filter(([target]) => Boolean(target)));
|
|
271
|
+
const references = versionBearingGitHubReferences(source).filter((item) => byTarget.has(item.target));
|
|
272
|
+
const diagnostics = [];
|
|
273
|
+
const actions = [];
|
|
274
|
+
for (const reference of references) {
|
|
275
|
+
const fact = byTarget.get(reference.target) || {};
|
|
276
|
+
const exact = fact.exact || {};
|
|
277
|
+
const latest = fact.latest || {};
|
|
278
|
+
const exactState = String(exact.state || 'unavailable');
|
|
279
|
+
const latestState = String(latest.state || 'unavailable');
|
|
280
|
+
const exactSha = String(exact.sha256 || '');
|
|
281
|
+
const latestSha = String(latest.sha256 || '');
|
|
282
|
+
const latestTarget = String(latest.target || '').trim();
|
|
283
|
+
let diagnostic = null;
|
|
284
|
+
let actionTitle = '';
|
|
285
|
+
if (exactState === 'missing') {
|
|
286
|
+
diagnostic = {
|
|
287
|
+
severity: 'error',
|
|
288
|
+
code: 'reference.permalink.unresolved',
|
|
289
|
+
message: `${reference.field} permalink does not resolve at its declared revision.`,
|
|
290
|
+
params: { field: reference.field, line: reference.line }
|
|
291
|
+
};
|
|
292
|
+
if (latestState === 'resolved' && latestTarget) actionTitle = `Repair ${reference.field} permalink to latest`;
|
|
293
|
+
} else if (exactState === 'unavailable') {
|
|
294
|
+
diagnostic = {
|
|
295
|
+
severity: 'warning',
|
|
296
|
+
code: 'reference.permalink.verification-unavailable',
|
|
297
|
+
message: `${reference.field} permalink could not be verified from the current host.`,
|
|
298
|
+
params: { field: reference.field, line: reference.line }
|
|
299
|
+
};
|
|
300
|
+
if (latestState === 'resolved' && latestTarget) actionTitle = `Use latest ${reference.field} permalink`;
|
|
301
|
+
} else if (exactState === 'resolved' && latestState === 'resolved' && exactSha && latestSha && exactSha !== latestSha) {
|
|
302
|
+
diagnostic = {
|
|
303
|
+
severity: 'warning',
|
|
304
|
+
code: 'reference.permalink.stale',
|
|
305
|
+
message: `${reference.field} permalink resolves, but master contains different bytes.`,
|
|
306
|
+
params: { field: reference.field, line: reference.line }
|
|
307
|
+
};
|
|
308
|
+
if (latestTarget) actionTitle = `Upgrade ${reference.field} permalink to latest`;
|
|
309
|
+
}
|
|
310
|
+
if (diagnostic) diagnostics.push(projectDiagnostic(diagnostic, source));
|
|
311
|
+
if (!actionTitle || !latestTarget || latestTarget === reference.target) continue;
|
|
312
|
+
const replacement = replaceReferenceTargetAtLine(source, reference, latestTarget);
|
|
313
|
+
if (!replacement || replacement === source) continue;
|
|
314
|
+
const sealed = sealIfSelfIntegrityPresent(replacement);
|
|
315
|
+
if (sealed.state === 'blocked') continue;
|
|
316
|
+
const replacementMarkdown = sealed.markdown;
|
|
317
|
+
actions.push(freeze({
|
|
318
|
+
id: `upgrade-permalink-${reference.field.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${reference.line}`,
|
|
319
|
+
title: actionTitle,
|
|
320
|
+
kind: 'replace-document',
|
|
321
|
+
qualification: 'deterministic-shared-core+explicit-host-resolution',
|
|
322
|
+
sourceSha256: sha256Hex(new TextEncoder().encode(source)),
|
|
323
|
+
replacementMarkdown,
|
|
324
|
+
diagnosticCodes: diagnostic ? [diagnostic.code] : [],
|
|
325
|
+
boundary: 'Uses explicit host-provided resolution evidence for the exact declared GitHub permalink and master candidate. A newer master revision is only offered as an operator-selected Quick Fix; it does not silently replace version-bearing source authority.'
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
return freeze({ diagnostics, actions });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function versionBearingGitHubReferences(markdown = '') {
|
|
332
|
+
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
333
|
+
const refs = [];
|
|
334
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
335
|
+
const line = String(lines[index] || '');
|
|
336
|
+
const fieldMatch = line.match(/^\s*-\s+(Envelope Schema|Parent Schema|Current Schema)\s*:\s*\[[^\]]+\]\((https:\/\/github\.com\/[^)]+\/blob\/[^)]+)\)\s*$/i);
|
|
337
|
+
if (fieldMatch) refs.push({ field: fieldMatch[1], target: fieldMatch[2], line: index + 1 });
|
|
338
|
+
if (/^\s*-\s+\[sha256-base64url-c14n-v2\]\(/.test(line)) {
|
|
339
|
+
const methodMatch = line.match(/^\s*-\s+\[sha256-base64url-c14n-v2\]\((https:\/\/github\.com\/[^)]+\/blob\/[^)]+)\)\s*$/i);
|
|
340
|
+
if (methodMatch) refs.push({ field: 'Integrity method', target: methodMatch[1], line: index + 1 });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return refs;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function replaceReferenceTargetAtLine(markdown = '', reference = {}, latestTarget = '') {
|
|
347
|
+
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
348
|
+
const index = Number(reference.line || 0) - 1;
|
|
349
|
+
if (index < 0 || index >= lines.length) return '';
|
|
350
|
+
if (!lines[index].includes(reference.target)) return '';
|
|
351
|
+
lines[index] = lines[index].replace(reference.target, latestTarget);
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function sealIfSelfIntegrityPresent(markdown = '') {
|
|
356
|
+
const state = canonicalC14nV2SelfState(markdown);
|
|
357
|
+
if (state.state === 'unavailable') return freeze({ state: 'ready', markdown: String(markdown || '') });
|
|
358
|
+
if (!['verified', 'mismatch', 'prepared'].includes(state.state)) return freeze({ state: 'blocked', markdown: String(markdown || '') });
|
|
359
|
+
const sealed = sealC14nV2Self(markdown);
|
|
360
|
+
return sealed.state === 'sealed' ? freeze({ state: 'ready', markdown: sealed.markdown }) : freeze({ state: 'blocked', markdown: String(markdown || '') });
|
|
361
|
+
}
|
|
362
|
+
|
|
235
363
|
function normalizeMalformedWorkspaceQualifiedTarget(value = '') {
|
|
236
364
|
const raw = String(value || '').trim();
|
|
237
365
|
if (classifyParentRecoveryReference(raw).kind !== 'malformed-workspace-qualified') return raw;
|
|
@@ -281,6 +409,28 @@ function locatedLine(lines = [], index = -1, state = 'deterministic', basis = ''
|
|
|
281
409
|
export function locateFindingLine(finding = {}, markdown = '') {
|
|
282
410
|
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
283
411
|
const params = finding.params || finding;
|
|
412
|
+
const code = String(finding.code || '');
|
|
413
|
+
|
|
414
|
+
// Integrity findings often carry the generic field name `Value`. That field
|
|
415
|
+
// appears once per integrity relation, so resolve semantically-owned relation
|
|
416
|
+
// locations before generic field lookup.
|
|
417
|
+
if (code === 'portable.lineage-integrity.parent-target-mismatch') {
|
|
418
|
+
const parentValueIndex = primaryParentIntegrityValueLine(lines);
|
|
419
|
+
if (parentValueIndex >= 0) return locatedLine(lines, parentValueIndex, 'deterministic', 'continuity-integrity-primary-parent-value');
|
|
420
|
+
}
|
|
421
|
+
if (code === 'integrity.c14n-v2.mismatch' || code === 'integrity.c14n-v2.ambiguous' || code === 'portable.lineage-integrity.child-self-mismatch' || code === 'portable.lineage-integrity.child-self-unavailable') {
|
|
422
|
+
const selfValueIndex = primarySelfIntegrityValueLine(lines);
|
|
423
|
+
if (selfValueIndex >= 0) return locatedLine(lines, selfValueIndex, 'deterministic', 'continuity-integrity-primary-self-value');
|
|
424
|
+
}
|
|
425
|
+
if (code === 'integrity.method-reference.unqualified') {
|
|
426
|
+
const headingIndex = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
|
|
427
|
+
const methodIndex = lines.findIndex((line, index) => index > headingIndex && /^\s*-\s+\[sha256-base64url-c14n-v2\]\([^)]+\)\s*$/.test(line));
|
|
428
|
+
if (methodIndex >= 0) return locatedLine(lines, methodIndex, 'deterministic', 'continuity-integrity-method-reference');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const explicitLine = Number(params.line || 0);
|
|
432
|
+
if (Number.isInteger(explicitLine) && explicitLine > 0) return locatedLine(lines, explicitLine - 1, 'deterministic', 'explicit-resolution-line');
|
|
433
|
+
|
|
284
434
|
const field = String(params.field || '').trim();
|
|
285
435
|
const section = String(params.section || '').trim();
|
|
286
436
|
const heading = String(params.heading || '').replace(/^#{1,6}\s+/, '').trim();
|
|
@@ -295,19 +445,15 @@ export function locateFindingLine(finding = {}, markdown = '') {
|
|
|
295
445
|
const envelopeIndex = lines.findIndex((line) => new RegExp(`^\\s*-\\s+${escapeRegExp(owner)}(?:\\s*:.*)?\\s*$`, 'i').test(line));
|
|
296
446
|
if (envelopeIndex >= 0) return locatedLine(lines, envelopeIndex, 'deterministic-anchor', `envelope-owner:${owner}`);
|
|
297
447
|
}
|
|
298
|
-
const code = String(finding.code || '');
|
|
299
448
|
if (code.includes('schema.') || code.endsWith('.schema.mismatch') || code === 'audit.schema-authority.unqualified') {
|
|
300
449
|
const index = lines.findIndex((line) => /^\s*-\s+Current Schema\s*:/.test(line));
|
|
301
450
|
if (index >= 0) return locatedLine(lines, index, 'deterministic', 'current-schema-field');
|
|
302
451
|
}
|
|
303
|
-
if (code === 'integrity.method-reference.unqualified') {
|
|
304
|
-
const headingIndex = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
|
|
305
|
-
const methodIndex = lines.findIndex((line, index) => index > headingIndex && /^\s*-\s+\[sha256-base64url-c14n-v2\]\([^)]+\)\s*$/.test(line));
|
|
306
|
-
if (methodIndex >= 0) return locatedLine(lines, methodIndex, 'deterministic', 'continuity-integrity-method-reference');
|
|
307
|
-
}
|
|
308
452
|
if (code.includes('integrity') || /integrity|checksum|digest/i.test(String(finding.message || ''))) {
|
|
309
453
|
const headingIndex = lines.findIndex((line) => line.trim() === '# Continuity Integrity');
|
|
310
454
|
if (headingIndex >= 0) {
|
|
455
|
+
const selfValueIndex = primarySelfIntegrityValueLine(lines);
|
|
456
|
+
if (selfValueIndex >= 0 && /self-integrity|canonical artifact bytes|child-self/i.test(String(finding.message || ''))) return locatedLine(lines, selfValueIndex, 'deterministic', 'continuity-integrity-primary-self-value');
|
|
311
457
|
const index = lines.findIndex((line, i) => i > headingIndex && /^\s+-\s+Value\s*:/.test(line));
|
|
312
458
|
if (index >= 0) return locatedLine(lines, index, 'deterministic', 'continuity-integrity-value');
|
|
313
459
|
return locatedLine(lines, headingIndex, 'deterministic-anchor', 'continuity-integrity-heading');
|
|
@@ -326,6 +472,75 @@ export function locateFindingLine(finding = {}, markdown = '') {
|
|
|
326
472
|
return freeze({ state: 'unresolved', line: null, sourceRange: null, basis: 'shared-finding-has-no-deterministic-line-evidence' });
|
|
327
473
|
}
|
|
328
474
|
|
|
475
|
+
function primaryParentIntegrityValueLine(lines = []) {
|
|
476
|
+
const headingIndex = lines.findIndex((line) => String(line || '').trim() === '# Continuity Integrity');
|
|
477
|
+
if (headingIndex < 0) return -1;
|
|
478
|
+
let parentTowards = -1;
|
|
479
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
480
|
+
const line = String(lines[index] || '');
|
|
481
|
+
if (index > headingIndex + 1 && /^#\s+/.test(line)) break;
|
|
482
|
+
const towards = line.match(/^\s+-\s+Towards\s*:\s*(.+?)\s*$/i);
|
|
483
|
+
if (towards) {
|
|
484
|
+
parentTowards = String(towards[1] || '').trim().toLowerCase() === 'self' ? -1 : index;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (parentTowards >= 0 && /^\s+-\s+Value\s*:/.test(line)) return index;
|
|
488
|
+
if (parentTowards >= 0 && /^-\s+/.test(line)) parentTowards = -1;
|
|
489
|
+
}
|
|
490
|
+
return -1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function primarySelfIntegrityValueLine(lines = []) {
|
|
494
|
+
const headingIndex = lines.findIndex((line) => String(line || '').trim() === '# Continuity Integrity');
|
|
495
|
+
if (headingIndex < 0) return -1;
|
|
496
|
+
let selfTowards = -1;
|
|
497
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
498
|
+
const line = String(lines[index] || '');
|
|
499
|
+
if (index > headingIndex + 1 && /^#\s+/.test(line)) break;
|
|
500
|
+
if (/^\s+-\s+Towards\s*:\s*self\s*$/i.test(line)) { selfTowards = index; continue; }
|
|
501
|
+
if (selfTowards >= 0 && /^\s+-\s+Value\s*:/.test(line)) return index;
|
|
502
|
+
if (selfTowards >= 0 && /^-\s+/.test(line)) selfTowards = -1;
|
|
503
|
+
}
|
|
504
|
+
return -1;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function deterministicParentIntegrityRepair(markdown = '', artifact = null) {
|
|
508
|
+
const source = String(markdown || '');
|
|
509
|
+
if (!source || !artifact || artifact.state !== 'parent-target-mismatch') return freeze({ state: 'unavailable' });
|
|
510
|
+
if (String(artifact?.parentTarget?.reason || '') !== 'target-self-digest-mismatch') return freeze({ state: 'unavailable' });
|
|
511
|
+
if (artifact?.parentAvailability?.state !== 'resolved' || artifact?.parentPrimarySelf?.state !== 'verified') return freeze({ state: 'unavailable' });
|
|
512
|
+
const declaredTarget = String(artifact?.parentTarget?.declaredTarget || '').trim();
|
|
513
|
+
const expectedTarget = String(artifact?.exactParent?.expectedIntegrityTarget || '').trim();
|
|
514
|
+
const digest = String(artifact?.repairCandidate?.candidateTargetDigest || artifact?.parentPrimarySelf?.value || '').trim();
|
|
515
|
+
if (!declaredTarget || !expectedTarget || declaredTarget !== expectedTarget || !digest) return freeze({ state: 'unavailable' });
|
|
516
|
+
|
|
517
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
518
|
+
const headingIndex = lines.findIndex((line) => String(line || '').trim() === '# Continuity Integrity');
|
|
519
|
+
if (headingIndex < 0) return freeze({ state: 'unavailable' });
|
|
520
|
+
let targetMatched = false;
|
|
521
|
+
let changed = false;
|
|
522
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
523
|
+
const line = String(lines[index] || '');
|
|
524
|
+
if (index > headingIndex + 1 && /^#\s+/.test(line)) break;
|
|
525
|
+
const towards = line.match(/^\s+-\s+Towards\s*:\s*(?:\[[^\]]+\]\(([^)]+)\)|(\S.*))\s*$/i);
|
|
526
|
+
if (towards) {
|
|
527
|
+
const value = String(towards[1] || towards[2] || '').trim();
|
|
528
|
+
targetMatched = value === declaredTarget;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (targetMatched && /^(\s+-\s+Value\s*:\s*)(.*)$/.test(line)) {
|
|
532
|
+
const match = line.match(/^(\s+-\s+Value\s*:\s*)(.*)$/);
|
|
533
|
+
lines[index] = `${match?.[1] || ' - Value: '}${digest}`;
|
|
534
|
+
changed = String(match?.[2] || '').trim() !== digest;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!changed) return freeze({ state: 'unavailable' });
|
|
539
|
+
const sealed = sealC14nV2Self(lines.join('\n'));
|
|
540
|
+
if (sealed.state !== 'sealed' && sealed.state !== 'unchanged') return freeze({ state: 'unavailable' });
|
|
541
|
+
return freeze({ state: 'ready', markdown: String(sealed.markdown || lines.join('\n')) });
|
|
542
|
+
}
|
|
543
|
+
|
|
329
544
|
function deterministicIntegrityHygieneRepair(markdown = '', findings = []) {
|
|
330
545
|
const source = String(markdown || '');
|
|
331
546
|
const codes = new Set((findings || []).map((item) => String(item?.code || '')));
|
|
@@ -357,7 +572,9 @@ function deterministicIntegrityHygieneRepair(markdown = '', findings = []) {
|
|
|
357
572
|
const diagnosticCodes = [
|
|
358
573
|
...(methodReferenceChanged ? ['integrity.method-reference.unqualified'] : []),
|
|
359
574
|
'integrity.c14n-v2.mismatch',
|
|
360
|
-
'integrity.c14n-v2.ambiguous'
|
|
575
|
+
'integrity.c14n-v2.ambiguous',
|
|
576
|
+
'portable.lineage-integrity.child-self-mismatch',
|
|
577
|
+
'portable.lineage-integrity.child-self-unavailable'
|
|
361
578
|
];
|
|
362
579
|
return freeze({ state: 'ready', markdown: sealed.markdown, methodReferenceChanged, diagnosticCodes: [...new Set(diagnosticCodes)] });
|
|
363
580
|
}
|
|
@@ -223,11 +223,15 @@ function choosePrimaryState({ parentResolution, parentSchema, parentPrimarySelf,
|
|
|
223
223
|
|| consider(parentSchema.state !== 'verified', 'parent-schema-unavailable', parentSchema.reason)
|
|
224
224
|
|| consider(parentPrimarySelf.state === 'mismatch', 'parent-self-mismatch', parentPrimarySelf.reason)
|
|
225
225
|
|| consider(parentPrimarySelf.state !== 'verified', 'parent-self-unavailable', parentPrimarySelf.reason)
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// Parent-target corruption is more specific than the consequential child-self
|
|
227
|
+
// mismatch caused by changing footer bytes. Surface the owned Parent relation
|
|
228
|
+
// first so editors can locate and repair the actual mutated digest.
|
|
228
229
|
|| consider(targetInspection.state === 'missing', 'parent-target-missing', targetInspection.reason)
|
|
229
230
|
|| consider(targetInspection.state === 'mismatch', 'parent-target-mismatch', targetInspection.reason)
|
|
230
|
-
|| consider(targetInspection.state
|
|
231
|
+
|| consider(targetInspection.state === 'ambiguous', 'parent-target-ambiguous', targetInspection.reason)
|
|
232
|
+
|| consider(childSelf.state === 'mismatch', 'child-self-mismatch', childSelf.reason)
|
|
233
|
+
|| consider(childSelf.state !== 'verified', 'child-self-unavailable', childSelf.reason)
|
|
234
|
+
|| consider(targetInspection.state !== 'verified', 'unsupported', targetInspection.reason)
|
|
231
235
|
|| consider(publicationOrigin.state === 'contradictory', 'publication-origin-contradictory', publicationOrigin.reason)
|
|
232
236
|
|| consider(publicationOrigin.state === 'stale', 'publication-origin-stale', publicationOrigin.reason)
|
|
233
237
|
|| consider(publicationOrigin.state === 'unresolved', 'publication-origin-unresolved', publicationOrigin.reason)
|