@weftlet/toolkit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/application/application.js +7 -4
- package/dist/application/cache-adoption.d.ts +43 -0
- package/dist/application/cache-adoption.js +129 -0
- package/dist/application/extraction-identity.d.ts +6 -0
- package/dist/application/extraction-identity.js +15 -0
- package/dist/application/hydration-failure.d.ts +41 -0
- package/dist/application/hydration-failure.js +43 -0
- package/dist/application/hydration-plan.d.ts +4 -7
- package/dist/application/hydration-plan.js +7 -15
- package/dist/application/hydrator.d.ts +3 -5
- package/dist/application/hydrator.js +23 -48
- package/dist/application/overlay.js +10 -3
- package/dist/application/saved-evidence.d.ts +6 -3
- package/dist/application/saved-evidence.js +7 -1
- package/dist/application/semantic-artifact-store.d.ts +4 -0
- package/dist/application/semantic-artifact-store.js +7 -0
- package/dist/application/semantic-run.d.ts +29 -0
- package/dist/application/semantic-run.js +111 -0
- package/dist/application/semantic-scope.d.ts +13 -0
- package/dist/application/semantic-scope.js +12 -0
- package/dist/application/snapshot-checks.d.ts +2 -1
- package/dist/application/snapshot-checks.js +8 -1
- package/dist/configuration/cache.d.ts +6 -0
- package/dist/configuration/cache.js +18 -0
- package/dist/extraction/document.js +1 -1
- package/dist/extraction/providers.js +10 -3
- package/dist/extraction/repository-nodes.d.ts +2 -3
- package/dist/extraction/repository-nodes.js +5 -11
- package/dist/extraction/routing.d.ts +1 -1
- package/dist/extraction/routing.js +3 -6
- package/dist/extraction/semantic-cache.d.ts +1 -0
- package/dist/extraction/semantic-cache.js +9 -2
- package/dist/extraction/semantic-diagnostics.d.ts +9 -0
- package/dist/extraction/semantic-diagnostics.js +15 -0
- package/dist/extraction/semantic.d.ts +5 -1
- package/dist/extraction/semantic.js +39 -6
- package/dist/extraction/static.d.ts +13 -5
- package/dist/extraction/static.js +8 -15
- package/dist/governance/governance.js +11 -7
- package/dist/graph/query.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/interfaces/cli.js +44 -21
- package/dist/storage/storage.d.ts +7 -1
- package/dist/storage/storage.js +15 -3
- package/dist/visualization/visualization.js +1 -0
- package/docs/ADR-016-hydration-preview.md +1 -1
- package/docs/ADR-017-shacl-validation-union.md +1 -1
- package/docs/ADR-018-operational-semantic-cache.md +100 -0
- package/docs/ADR-019-semantic-failure-diagnostics.md +82 -0
- package/docs/ADR-020-canonical-physical-file-producers.md +97 -0
- package/docs/PROVIDERS.md +51 -1
- package/docs/RELEASE-1.0-PLAN.md +70 -21
- package/docs/RELEASING.md +4 -2
- package/docs/ROADMAP.md +31 -12
- package/docs/VALIDATION.md +212 -4
- package/docs/extraction-manifest.json +42 -21
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@ import { sha256, stableJson } from "../infrastructure/util.js";
|
|
|
11
11
|
import { canonicalizeRepositoryDocumentNode } from './repository-nodes.js';
|
|
12
12
|
import { semanticArtifactKey, semanticArtifactOutputDigest, semanticProviderIdentity } from './semantic-identity.js';
|
|
13
13
|
import { inspectSemanticCache } from './semantic-cache.js';
|
|
14
|
+
import { SemanticPolicyError } from './semantic-diagnostics.js';
|
|
14
15
|
import { assertSemanticCitation, numberedSemanticSource, semanticCitationSchema, } from "./semantic-citations.js";
|
|
15
16
|
function canonicalizeRepositoryDocuments(output) {
|
|
16
17
|
const ids = new Map();
|
|
@@ -334,14 +335,32 @@ export class SemanticHydrator {
|
|
|
334
335
|
this.extractor = extractor;
|
|
335
336
|
}
|
|
336
337
|
async hydrate(source, context, forceRevision, options = {}) {
|
|
337
|
-
|
|
338
|
+
const checkpoint = () => {
|
|
339
|
+
if (options.signal?.aborted)
|
|
340
|
+
throw new SemanticProviderError('cancelled', 'Semantic extraction cancelled');
|
|
341
|
+
};
|
|
342
|
+
checkpoint();
|
|
343
|
+
options.onPhase?.('semantic-cache-read');
|
|
338
344
|
const key = semanticArtifactKey(this.config, source, this.extractor.identity, forceRevision);
|
|
339
345
|
const cached = await inspectSemanticCache(this.store, this.config, source, this.extractor.identity, forceRevision);
|
|
340
|
-
if (cached.status === 'hit')
|
|
341
|
-
|
|
346
|
+
if (cached.status === 'hit') {
|
|
347
|
+
// Planning remains read-only. Execution can copy an inspected legacy hit
|
|
348
|
+
// atomically without rewriting its original artifact or extraction identity.
|
|
349
|
+
checkpoint();
|
|
350
|
+
if (cached.origin === 'legacy')
|
|
351
|
+
options.onPhase?.('semantic-cache-write');
|
|
352
|
+
const artifact = cached.origin === 'legacy'
|
|
353
|
+
? await this.promoteLegacy(source, cached.artifact, forceRevision) : cached.artifact;
|
|
354
|
+
return { artifact, reused: true };
|
|
355
|
+
}
|
|
342
356
|
if (cached.status !== 'miss')
|
|
343
357
|
throw new Error(cached.reason);
|
|
344
|
-
|
|
358
|
+
checkpoint();
|
|
359
|
+
options.onPhase?.('semantic-extract');
|
|
360
|
+
// Phase observation belongs to hydration, not the provider extension contract.
|
|
361
|
+
const extraction = await this.extractor.extract(source, context, { signal: options.signal, timeoutMs: options.timeoutMs });
|
|
362
|
+
checkpoint();
|
|
363
|
+
options.onPhase?.('semantic-validate');
|
|
345
364
|
assertSemanticOutput(extraction.parsed, source);
|
|
346
365
|
if (typeof extraction.rawModelOutput !== 'string' || Buffer.byteLength(extraction.rawModelOutput) > 2 * 1024 * 1024) {
|
|
347
366
|
throw new SemanticProviderError('invalid-output', 'Invalid or oversized raw semantic output');
|
|
@@ -351,7 +370,7 @@ export class SemanticHydrator {
|
|
|
351
370
|
const facts = output.assertions.map((assertion, index) => {
|
|
352
371
|
assertSemanticCitation(assertion.citation, source.path, lineCount, `assertion[${index}]`);
|
|
353
372
|
if (assertion.confidence < this.config.minimumConfidence) {
|
|
354
|
-
throw new
|
|
373
|
+
throw new SemanticPolicyError('low-confidence', `Low-confidence semantic assertion for ${source.path}`);
|
|
355
374
|
}
|
|
356
375
|
const provenance = {
|
|
357
376
|
path: source.path,
|
|
@@ -398,7 +417,7 @@ export class SemanticHydrator {
|
|
|
398
417
|
});
|
|
399
418
|
const unresolvedMappings = output.unresolvedMappings ?? [];
|
|
400
419
|
if (this.config.failOnUnresolved && (unresolvedMappings.length > 0 || diagnostics.length > 0)) {
|
|
401
|
-
throw new
|
|
420
|
+
throw new SemanticPolicyError('unresolved', `Unresolved semantic mappings for ${source.path}: ${[...unresolvedMappings, ...diagnostics.map((item) => item.reason)].join('; ')}`);
|
|
402
421
|
}
|
|
403
422
|
const compiledOml = renderOml(key, output.assertions);
|
|
404
423
|
const outputDigest = semanticArtifactOutputDigest({ facts, diagnostics, oml: compiledOml, rawModelOutput: extraction.rawModelOutput });
|
|
@@ -425,7 +444,21 @@ export class SemanticHydrator {
|
|
|
425
444
|
rawModelOutput: extraction.rawModelOutput,
|
|
426
445
|
outputDigest,
|
|
427
446
|
};
|
|
447
|
+
checkpoint();
|
|
448
|
+
options.onPhase?.('semantic-cache-write');
|
|
428
449
|
await this.store.write(artifact);
|
|
429
450
|
return { artifact, reused: false };
|
|
430
451
|
}
|
|
452
|
+
async promoteLegacy(source, artifact, forceRevision) {
|
|
453
|
+
await this.store.write(artifact);
|
|
454
|
+
// A concurrent winner must satisfy the full contract, not merely expose the
|
|
455
|
+
// same outputDigest field. Never publish using an invalid retained artifact.
|
|
456
|
+
const retained = await inspectSemanticCache(this.store, this.config, source, this.extractor.identity, forceRevision);
|
|
457
|
+
if (retained.status !== 'hit')
|
|
458
|
+
throw new Error(retained.reason);
|
|
459
|
+
if (retained.origin !== 'primary' || retained.artifact.outputDigest !== artifact.outputDigest) {
|
|
460
|
+
throw new Error('Concurrent semantic artifact changed during legacy promotion; inspect cache integrity before retrying.');
|
|
461
|
+
}
|
|
462
|
+
return retained.artifact;
|
|
463
|
+
}
|
|
431
464
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextGraphConfig } from "../configuration/config.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type ExtractionContext, type FactExtractor, type GraphFact } from "@weftlet/core";
|
|
3
3
|
import { GitRepository } from "../repository/git.js";
|
|
4
4
|
export interface ScipIndexResult {
|
|
5
5
|
readonly path: string;
|
|
@@ -17,12 +17,16 @@ export declare class ScipTypeScriptIndexer implements StaticIndexer {
|
|
|
17
17
|
index(context: ExtractionContext): Promise<ScipIndexResult>;
|
|
18
18
|
}
|
|
19
19
|
export declare function normalizeScipCheckout(bytes: Uint8Array, checkoutRoot: string): Uint8Array;
|
|
20
|
+
export declare const scipSymbolExtractorIdentity: {
|
|
21
|
+
readonly id: "scip-symbol-extractor";
|
|
22
|
+
readonly version: "1.1.0";
|
|
23
|
+
};
|
|
20
24
|
export declare class ScipSymbolExtractor implements FactExtractor {
|
|
21
25
|
private readonly repository;
|
|
22
26
|
private readonly index;
|
|
23
27
|
readonly identity: {
|
|
24
|
-
id:
|
|
25
|
-
version:
|
|
28
|
+
readonly id: "scip-symbol-extractor";
|
|
29
|
+
readonly version: "1.1.0";
|
|
26
30
|
};
|
|
27
31
|
constructor(repository: GitRepository, index: ScipIndexResult);
|
|
28
32
|
extract(context: ExtractionContext): Promise<readonly GraphFact[]>;
|
|
@@ -42,14 +46,18 @@ export declare class RepositoryFileExtractor implements FactExtractor {
|
|
|
42
46
|
constructor(repository: GitRepository, roots: readonly string[], scipDigest: string);
|
|
43
47
|
extract(context: ExtractionContext): Promise<readonly GraphFact[]>;
|
|
44
48
|
}
|
|
49
|
+
export declare const repositoryContractExtractorIdentity: {
|
|
50
|
+
readonly id: "repository-contract-extractor";
|
|
51
|
+
readonly version: "1.2.0";
|
|
52
|
+
};
|
|
45
53
|
export declare class RepositoryContractExtractor implements FactExtractor {
|
|
46
54
|
private readonly repository;
|
|
47
55
|
private readonly patterns;
|
|
48
56
|
private readonly consumerRoots;
|
|
49
57
|
private readonly aliases;
|
|
50
58
|
readonly identity: {
|
|
51
|
-
id:
|
|
52
|
-
version:
|
|
59
|
+
readonly id: "repository-contract-extractor";
|
|
60
|
+
readonly version: "1.2.0";
|
|
53
61
|
};
|
|
54
62
|
constructor(repository: GitRepository, patterns: readonly string[], consumerRoots: readonly string[], aliases?: Readonly<Record<string, readonly string[]>>);
|
|
55
63
|
extract(context: ExtractionContext): Promise<readonly GraphFact[]>;
|
|
@@ -5,6 +5,7 @@ import { copyFile, mkdir, readFile, rename, stat, writeFile } from 'node:fs/prom
|
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { repositoryFileNode } from "@weftlet/core";
|
|
8
9
|
import { GitRepository } from "../repository/git.js";
|
|
9
10
|
import { assertScipSourceProject, assertSourceProjectInvocation } from '../configuration/scip-project.js';
|
|
10
11
|
import { prepareScipSourceProject } from './scip-project.js';
|
|
@@ -90,10 +91,11 @@ export function normalizeScipCheckout(bytes, checkoutRoot) {
|
|
|
90
91
|
}
|
|
91
92
|
return index.serializeBinary();
|
|
92
93
|
}
|
|
94
|
+
export const scipSymbolExtractorIdentity = { id: 'scip-symbol-extractor', version: '1.1.0' };
|
|
93
95
|
export class ScipSymbolExtractor {
|
|
94
96
|
repository;
|
|
95
97
|
index;
|
|
96
|
-
identity =
|
|
98
|
+
identity = scipSymbolExtractorIdentity;
|
|
97
99
|
constructor(repository, index) {
|
|
98
100
|
this.repository = repository;
|
|
99
101
|
this.index = index;
|
|
@@ -105,11 +107,7 @@ export class ScipSymbolExtractor {
|
|
|
105
107
|
const path = document.relative_path.replace(/\\/g, '/');
|
|
106
108
|
const content = await this.repository.readFileAt(context.commitSha, path);
|
|
107
109
|
const digest = sha256(content);
|
|
108
|
-
const component =
|
|
109
|
-
id: `component:file:${path}`,
|
|
110
|
-
kind: (/\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component'),
|
|
111
|
-
label: path,
|
|
112
|
-
};
|
|
110
|
+
const component = repositoryFileNode(path);
|
|
113
111
|
for (const occurrence of document.occurrences) {
|
|
114
112
|
if (!occurrence.symbol || occurrence.symbol.startsWith('local '))
|
|
115
113
|
continue;
|
|
@@ -195,9 +193,7 @@ export class RepositoryFileExtractor {
|
|
|
195
193
|
const content = await this.repository.readFileAt(context.commitSha, path);
|
|
196
194
|
const digest = sha256(content);
|
|
197
195
|
const component = {
|
|
198
|
-
|
|
199
|
-
kind: (/\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component'),
|
|
200
|
-
label: path,
|
|
196
|
+
...repositoryFileNode(path),
|
|
201
197
|
attributes: { scipIndexDigest: this.scipDigest },
|
|
202
198
|
};
|
|
203
199
|
const provenance = {
|
|
@@ -239,12 +235,13 @@ export class RepositoryFileExtractor {
|
|
|
239
235
|
return facts;
|
|
240
236
|
}
|
|
241
237
|
}
|
|
238
|
+
export const repositoryContractExtractorIdentity = { id: 'repository-contract-extractor', version: '1.2.0' };
|
|
242
239
|
export class RepositoryContractExtractor {
|
|
243
240
|
repository;
|
|
244
241
|
patterns;
|
|
245
242
|
consumerRoots;
|
|
246
243
|
aliases;
|
|
247
|
-
identity =
|
|
244
|
+
identity = repositoryContractExtractorIdentity;
|
|
248
245
|
constructor(repository, patterns, consumerRoots, aliases = {}) {
|
|
249
246
|
this.repository = repository;
|
|
250
247
|
this.patterns = patterns;
|
|
@@ -306,11 +303,7 @@ export class RepositoryContractExtractor {
|
|
|
306
303
|
digest: sha256(consumerContent),
|
|
307
304
|
commitSha: context.commitSha,
|
|
308
305
|
};
|
|
309
|
-
const consumer =
|
|
310
|
-
id: `component:file:${match.path}`,
|
|
311
|
-
kind: 'Component',
|
|
312
|
-
label: match.path,
|
|
313
|
-
};
|
|
306
|
+
const consumer = repositoryFileNode(match.path);
|
|
314
307
|
facts.push({
|
|
315
308
|
id: sha256(stableJson({ consumer, contract, consumerProvenance })),
|
|
316
309
|
subject: consumer,
|
|
@@ -2,10 +2,12 @@ import { readFile, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
5
6
|
import { GitRepository } from "../repository/git.js";
|
|
6
7
|
import { RdfEvidenceGraph, CG, nodeIri } from "../graph/rdf.js";
|
|
7
8
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
8
9
|
import { runProcess } from "../infrastructure/process.js";
|
|
10
|
+
const architectureAnalyzerVersion = '3';
|
|
9
11
|
export async function analyzeArchitecture(root, policy, policyPath = 'architecture.policy.json') {
|
|
10
12
|
const findings = [];
|
|
11
13
|
const add = (category, ruleId, from, to, message) => {
|
|
@@ -86,11 +88,11 @@ export async function analyzeArchitecture(root, policy, policyPath = 'architectu
|
|
|
86
88
|
const lines = content.split('\n');
|
|
87
89
|
facts.push({
|
|
88
90
|
id: sha256(stableJson({ from: module.source, to: dependency.resolved, digest: inputs[module.source] })),
|
|
89
|
-
subject: {
|
|
91
|
+
subject: { ...repositoryFileNode(module.source),
|
|
90
92
|
attributes: membership[0] ? { domain: membership[0].domain, layer: membership[0].layer,
|
|
91
93
|
module: membership[0].id, adapter: Boolean(membership[0].adapter) } : {} },
|
|
92
94
|
predicate: 'consumes',
|
|
93
|
-
object:
|
|
95
|
+
object: repositoryFileNode(dependency.resolved),
|
|
94
96
|
provenance: { path: module.source, startLine: 1, endLine: lines.length, digest: inputs[module.source], commitSha },
|
|
95
97
|
extractor: { id: 'dependency-cruiser', version: '18.7.0' }, confidence: 1,
|
|
96
98
|
authority: dirtyPaths.has(module.source) ? 'draft' : 'implemented', snapshotId: 'pending',
|
|
@@ -128,7 +130,7 @@ export async function analyzeArchitecture(root, policy, policyPath = 'architectu
|
|
|
128
130
|
attributes: { domain: module.domain, layer: module.layer, adapter: Boolean(module.adapter) } };
|
|
129
131
|
addPolicyFact(node, 'governedBy', { id: `layer:${module.layer}`, kind: 'Scope', label: module.layer });
|
|
130
132
|
for (const path of Object.keys(inputs).filter((path) => new RegExp(module.path).test(path))) {
|
|
131
|
-
addPolicyFact(node, 'contains',
|
|
133
|
+
addPolicyFact(node, 'contains', repositoryFileNode(path));
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
for (const contract of policy.contracts ?? []) {
|
|
@@ -137,14 +139,16 @@ export async function analyzeArchitecture(root, policy, policyPath = 'architectu
|
|
|
137
139
|
if (!Object.keys(inputs).length)
|
|
138
140
|
add('extraction-gap', 'source-coverage', '', '', 'No source files were analyzed');
|
|
139
141
|
const contentDigest = sha256(stableJson({ inputs, policyDigest, commitSha, dirtyPaths: [...dirtyPaths].sort(),
|
|
140
|
-
tools: { dependencyCruiser: '18.7.0', eslint: '10.13.0', jscpd: '5.4.1', analyzer:
|
|
142
|
+
tools: { dependencyCruiser: '18.7.0', eslint: '10.13.0', jscpd: '5.4.1', analyzer: architectureAnalyzerVersion } }));
|
|
141
143
|
const snapshotId = `architecture-${contentDigest}`;
|
|
142
144
|
const snapshot = {
|
|
143
145
|
manifest: {
|
|
144
146
|
schemaVersion: 1, repositoryId: 'architecture-analysis', ref: 'HEAD', commitSha, snapshotId,
|
|
145
147
|
configurationDigest: policyDigest, scope: policy.roots, contentDigest,
|
|
146
148
|
...(dirtyPaths.size ? { baseSha: commitSha, overlayDigest: contentDigest } : {}),
|
|
147
|
-
ontologyVersion: 'architecture-v1', extractorVersions: {
|
|
149
|
+
ontologyVersion: 'architecture-v1', extractorVersions: {
|
|
150
|
+
'dependency-cruiser': '18.7.0', 'architecture-analyzer': architectureAnalyzerVersion,
|
|
151
|
+
},
|
|
148
152
|
semanticModel: 'none', semanticPromptVersion: 'none', generatedAt: new Date().toISOString(),
|
|
149
153
|
semanticArtifactKeys: [], scipIndexDigest: '', rdfDigest: '',
|
|
150
154
|
},
|
|
@@ -158,8 +162,8 @@ export async function analyzeArchitecture(root, policy, policyPath = 'architectu
|
|
|
158
162
|
if (result.truncated)
|
|
159
163
|
add('extraction-gap', rule.id, '', '', 'Transitive dependency query truncated');
|
|
160
164
|
for (const row of result.rows) {
|
|
161
|
-
const from = modules.find((m) => nodeIri(
|
|
162
|
-
const to = modules.find((m) => nodeIri(
|
|
165
|
+
const from = modules.find((m) => nodeIri(repositoryFileNode(m.source).id) === row.from)?.source;
|
|
166
|
+
const to = modules.find((m) => nodeIri(repositoryFileNode(m.source).id) === row.to)?.source;
|
|
163
167
|
if (from && to && new RegExp(rule.from).test(from) && new RegExp(rule.to).test(to)) {
|
|
164
168
|
add(rule.approved ? 'rule-violation' : 'advisory', rule.id, from, to, 'Transitive dependency inferred by SPARQL property path from source import evidence');
|
|
165
169
|
}
|
package/dist/graph/query.js
CHANGED
|
@@ -113,6 +113,7 @@ export class MarkdownContextPackRenderer {
|
|
|
113
113
|
] : []),
|
|
114
114
|
...(pack.evidenceState ? [
|
|
115
115
|
`Stale relative to checkout: ${pack.evidenceState.stale}; configuration matches: ${pack.evidenceState.configurationMatches}`,
|
|
116
|
+
`Extraction compatibility: ${pack.evidenceState.extractionCompatible === undefined ? 'not assessed' : pack.evidenceState.extractionCompatible ? 'current' : `refresh required (${pack.evidenceState.incompatibleExtractors?.join(', ') || 'unknown versions'})`}`,
|
|
116
117
|
`Excluded working changes: ${pack.evidenceState.excludedWorkingChanges.join(', ') || '(none)'}`,
|
|
117
118
|
...(pack.evidenceState.overlayFreshness ? [
|
|
118
119
|
`Overlay freshness: ${pack.evidenceState.overlayFreshness}`,
|
|
@@ -327,6 +328,7 @@ export class SnapshotImpactQueryService {
|
|
|
327
328
|
baseSha: snapshot.manifest.baseSha,
|
|
328
329
|
overlayDigest: snapshot.manifest.overlayDigest,
|
|
329
330
|
overlayCapture: snapshot.manifest.overlayCapture,
|
|
331
|
+
extractorVersions: snapshot.manifest.extractorVersions,
|
|
330
332
|
},
|
|
331
333
|
capabilities,
|
|
332
334
|
facts,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,6 @@ export * from "./application/overlay.js";
|
|
|
10
10
|
export * from "./graph/rdf.js";
|
|
11
11
|
export * from "./extraction/providers.js";
|
|
12
12
|
export * from './application/hydration-plan.js';
|
|
13
|
+
export * from './application/cache-adoption.js';
|
|
14
|
+
export * from './application/hydration-failure.js';
|
|
15
|
+
export * from './extraction/semantic-diagnostics.js';
|
package/dist/index.js
CHANGED
|
@@ -11,3 +11,6 @@ export * from "./application/overlay.js";
|
|
|
11
11
|
export * from "./graph/rdf.js";
|
|
12
12
|
export * from "./extraction/providers.js";
|
|
13
13
|
export * from './application/hydration-plan.js';
|
|
14
|
+
export * from './application/cache-adoption.js';
|
|
15
|
+
export * from './application/hydration-failure.js';
|
|
16
|
+
export * from './extraction/semantic-diagnostics.js';
|
package/dist/interfaces/cli.js
CHANGED
|
@@ -16,6 +16,8 @@ import { diagnoseSetup } from "./doctor.js";
|
|
|
16
16
|
import { runInit } from "./init.js";
|
|
17
17
|
import { loadConfig } from "../configuration/config.js";
|
|
18
18
|
import { planHydration } from '../application/hydration-plan.js';
|
|
19
|
+
import { adoptSemanticCache } from '../application/cache-adoption.js';
|
|
20
|
+
import { SemanticHydrationError } from '../application/hydration-failure.js';
|
|
19
21
|
function valueAfter(args, name) {
|
|
20
22
|
const index = args.indexOf(name);
|
|
21
23
|
return index >= 0 ? args[index + 1] : undefined;
|
|
@@ -59,7 +61,7 @@ function formatHydrationPlan(plan) {
|
|
|
59
61
|
`Source/cache/budget checks: ${readiness}`,
|
|
60
62
|
];
|
|
61
63
|
for (const document of plan.documents) {
|
|
62
|
-
lines.push(`- ${JSON.stringify(document.path)}: ${document.bytes} bytes, ${document.lines} lines; ${document.role}; source ${document.sourceAuthority}, extraction ${document.extractionReview}; cache ${document.cache.status}; selected by ${document.selectedBy.map((selector) => JSON.stringify(selector)).join(', ')}`);
|
|
64
|
+
lines.push(`- ${JSON.stringify(document.path)} [${document.sourceId}]: ${document.bytes} bytes, ${document.lines} lines; ${document.role}; source ${document.sourceAuthority}, extraction ${document.extractionReview}; cache ${document.cache.status}; selected by ${document.selectedBy.map((selector) => JSON.stringify(selector)).join(', ')}`);
|
|
63
65
|
}
|
|
64
66
|
for (const selection of plan.selections.filter((item) => item.status !== 'matched')) {
|
|
65
67
|
lines.push(`Selection ${JSON.stringify(selection.selector)}: ${selection.status}`);
|
|
@@ -80,7 +82,7 @@ function formatHydrationPlan(plan) {
|
|
|
80
82
|
async function main() {
|
|
81
83
|
const args = process.argv.slice(2);
|
|
82
84
|
if (args.includes('--help') || !args.length) {
|
|
83
|
-
process.stdout.write('weftlet init|config show|config validate|doctor|hydrate|overlay|impact scan|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Preview readiness is advisory and does not add a hydration gate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
|
|
85
|
+
process.stdout.write('weftlet init|config show|config validate|doctor|hydrate|cache adopt|overlay|impact scan|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Preview readiness is advisory and does not add a hydration gate.\nhydrate --json --quiet emits structured semantic failures on stderr with no progress lines; failure stdout stays empty. hydrate --plan maps opaque source IDs to local document paths.\ncache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run] returns JSON and copies only validated compatible artifacts, without model calls or snapshot writes. --dry-run performs no cache writes. Agent configurations accept the same --owner-doc, --task and --capability selection as hydrate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
if (args[0] === 'init') {
|
|
@@ -113,6 +115,20 @@ async function main() {
|
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
const configPath = valueAfter(args, '--config') ?? defaultConfigPath();
|
|
118
|
+
if (args[0] === 'cache') {
|
|
119
|
+
const fromConfigPath = valueAfter(args, '--from-config');
|
|
120
|
+
if (args[1] !== 'adopt' || !fromConfigPath || fromConfigPath.startsWith('--')) {
|
|
121
|
+
throw new Error('Usage: weftlet cache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run]');
|
|
122
|
+
}
|
|
123
|
+
const result = await adoptSemanticCache(configPath, {
|
|
124
|
+
fromConfigPath, ref: valueAfter(args, '--ref') ?? 'HEAD', dryRun: args.includes('--dry-run'),
|
|
125
|
+
request: impactRequestFromArgs(args),
|
|
126
|
+
});
|
|
127
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
128
|
+
if (!result.ready)
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
116
132
|
if (args[0] === 'doctor') {
|
|
117
133
|
process.stdout.write(`${JSON.stringify(await diagnoseSetup(configPath), null, 2)}\n`);
|
|
118
134
|
return;
|
|
@@ -163,24 +179,29 @@ async function main() {
|
|
|
163
179
|
const controller = new AbortController();
|
|
164
180
|
const cancel = () => controller.abort();
|
|
165
181
|
process.once('SIGINT', cancel);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
try {
|
|
183
|
+
const ref = valueAfter(args, '--ref') ?? 'HEAD';
|
|
184
|
+
const scope = await application.scopeResolver?.resolve(impactRequestFromArgs(args), ref);
|
|
185
|
+
const result = await application.hydrator.hydrate({
|
|
186
|
+
ref: scope?.commitSha ?? ref,
|
|
187
|
+
headName: scope?.headName ?? application.headName,
|
|
188
|
+
semanticSources: scope?.semanticSources,
|
|
189
|
+
forceSemantic: args.includes('--force-semantic'),
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
onProgress: args.includes('--quiet') ? undefined : (event) => process.stderr.write(`[context-graph] ${event.phase}${event.total === undefined ? '' : ` ${event.completed}/${event.total}`}` +
|
|
192
|
+
`${event.sourceId === undefined ? '' : ` ${event.sourceId}`}${event.elapsedMs === undefined ? '' : ` ${event.elapsedMs} ms`}\n`),
|
|
193
|
+
});
|
|
194
|
+
process.stdout.write(`${JSON.stringify({
|
|
195
|
+
snapshotId: result.snapshot.manifest.snapshotId,
|
|
196
|
+
commitSha: result.snapshot.manifest.commitSha,
|
|
197
|
+
contentDigest: result.snapshot.manifest.contentDigest,
|
|
198
|
+
semanticExtracted: result.semanticExtracted,
|
|
199
|
+
semanticReused: result.semanticReused,
|
|
200
|
+
}, null, 2)}\n`);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
process.removeListener('SIGINT', cancel);
|
|
204
|
+
}
|
|
184
205
|
return;
|
|
185
206
|
}
|
|
186
207
|
if (command === 'clean-compare') {
|
|
@@ -236,6 +257,8 @@ async function main() {
|
|
|
236
257
|
throw new Error('Usage: weftlet hydrate|clean-compare|impact scan|diagnostics show --snapshot <id>|invariant check|ontology compile|visualize --output <file.html|file.dot|file.svg>');
|
|
237
258
|
}
|
|
238
259
|
main().catch((error) => {
|
|
239
|
-
process.stderr.write(`${error instanceof
|
|
260
|
+
process.stderr.write(`${error instanceof SemanticHydrationError
|
|
261
|
+
? process.argv.includes('--json') ? JSON.stringify(error.report, null, 2) : error.message
|
|
262
|
+
: error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
240
263
|
process.exitCode = 1;
|
|
241
264
|
});
|
|
@@ -9,7 +9,13 @@ export declare class FileSnapshotStore implements SnapshotStore {
|
|
|
9
9
|
}
|
|
10
10
|
export declare class SemanticArtifactStore {
|
|
11
11
|
private readonly root;
|
|
12
|
-
|
|
12
|
+
private readonly legacyRoots;
|
|
13
|
+
constructor(root: string, legacyRoots?: readonly string[]);
|
|
13
14
|
read(key: string): Promise<SemanticArtifact | undefined>;
|
|
15
|
+
/** Only explicitly supplied roots are consulted; never enumerate other consumers' profiles. */
|
|
16
|
+
readEntry(key: string): Promise<{
|
|
17
|
+
artifact: SemanticArtifact;
|
|
18
|
+
origin: 'primary' | 'legacy';
|
|
19
|
+
} | undefined>;
|
|
14
20
|
write(artifact: SemanticArtifact): Promise<void>;
|
|
15
21
|
}
|
package/dist/storage/storage.js
CHANGED
|
@@ -87,11 +87,23 @@ export class FileSnapshotStore {
|
|
|
87
87
|
}
|
|
88
88
|
export class SemanticArtifactStore {
|
|
89
89
|
root;
|
|
90
|
-
|
|
90
|
+
legacyRoots;
|
|
91
|
+
constructor(root, legacyRoots = []) {
|
|
91
92
|
this.root = root;
|
|
93
|
+
this.legacyRoots = legacyRoots;
|
|
94
|
+
}
|
|
95
|
+
async read(key) {
|
|
96
|
+
return (await this.readEntry(key))?.artifact;
|
|
92
97
|
}
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
/** Only explicitly supplied roots are consulted; never enumerate other consumers' profiles. */
|
|
99
|
+
async readEntry(key) {
|
|
100
|
+
const filename = `${safeKey(key)}.json`;
|
|
101
|
+
for (const [index, root] of [this.root, ...this.legacyRoots].entries()) {
|
|
102
|
+
const artifact = await readJson(join(root, 'artifacts', 'semantic', filename));
|
|
103
|
+
if (artifact !== undefined)
|
|
104
|
+
return { artifact, origin: index === 0 ? 'primary' : 'legacy' };
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
95
107
|
}
|
|
96
108
|
async write(artifact) {
|
|
97
109
|
const path = join(this.root, 'artifacts', 'semantic', `${safeKey(artifact.key)}.json`);
|
|
@@ -394,6 +394,7 @@ export function renderContextPackHtml(pack, sourceUrlTemplate) {
|
|
|
394
394
|
<strong>${pack.snapshot.overlayDigest ? 'DRAFT overlay' : pack.evidenceState?.stale ? 'HISTORICAL evidence' : pack.evidenceState ? 'Saved evidence at current commit' : 'Saved evidence — current checkout not assessed'}</strong>
|
|
395
395
|
<p>Configuration: ${escapeXml(pack.snapshot.configurationDigest ?? 'unknown')}; ${pack.evidenceState ? (pack.evidenceState.configurationMatches ? 'matches current configuration' : 'configuration mismatch') : 'configuration match unknown'}.
|
|
396
396
|
Scope: ${escapeXml(pack.snapshot.scope?.join(', ') ?? 'unknown')}.
|
|
397
|
+
Extraction compatibility: ${pack.evidenceState?.extractionCompatible === undefined ? 'not assessed' : pack.evidenceState.extractionCompatible ? 'current' : `refresh required (${escapeXml(pack.evidenceState.incompatibleExtractors?.join(', ') ?? 'unknown versions')})`}.
|
|
397
398
|
Excluded working paths: ${pack.evidenceState?.excludedWorkingChanges.length ?? 'unknown'}.
|
|
398
399
|
${pack.snapshot.overlayDigest ? `Overlay freshness: ${escapeXml(pack.evidenceState?.overlayFreshness ?? 'unknown')}.` : ''}
|
|
399
400
|
This view does not validate the current diff.</p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR-016: Read-only hydration planning
|
|
2
2
|
|
|
3
|
-
Status: accepted and implemented
|
|
3
|
+
Status: accepted and implemented in 0.3.0; absent from 0.2.1.
|
|
4
4
|
See [validation](VALIDATION.md) for recorded publication evidence.
|
|
5
5
|
|
|
6
6
|
## Problem
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR-017: Validate a temporary RDF union without changing canonical evidence
|
|
2
2
|
|
|
3
|
-
Status: accepted and implemented
|
|
3
|
+
Status: accepted and implemented in the focused 0.3.0 release; absent from 0.2.1.
|
|
4
4
|
See [validation](VALIDATION.md) for checks and recorded publication evidence.
|
|
5
5
|
|
|
6
6
|
## Problem and measured mechanism
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ADR-018: Preserve Codex artifacts across operational configuration changes
|
|
2
|
+
|
|
3
|
+
Status: accepted and implemented in development source after 0.3.0; release evidence is recorded
|
|
4
|
+
in [validation](VALIDATION.md). This increment does not claim a new npm publication.
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
With `storageScope: "user"`, the storage profile includes the entire configuration and checkout
|
|
9
|
+
path. Increasing a timeout therefore selected a different directory even though the built-in
|
|
10
|
+
Codex artifact key already excluded that timeout. A partially completed run could lose access
|
|
11
|
+
to reusable work without any source or extraction-contract change.
|
|
12
|
+
|
|
13
|
+
Custom provider modules present a different boundary: they receive the entire semantic
|
|
14
|
+
configuration and may use it to affect their output. Their identities must remain conservative.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Keep `resolveStorageRoot`, snapshot profiles, snapshot IDs and full configuration digests unchanged.
|
|
19
|
+
For built-in Codex with user storage, use a separate semantic-artifact partition. Its identity
|
|
20
|
+
includes the resolved checkout and full configuration except these five semantic controls:
|
|
21
|
+
|
|
22
|
+
| Setting | Why it can share successful Codex artifacts |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `timeoutMs` | Bounds execution; it does not change the prompt or model arguments |
|
|
25
|
+
| `retries` | Limits retries of supported transient failures |
|
|
26
|
+
| `concurrency` | Schedules independent source extractions |
|
|
27
|
+
| `maxDocuments` | Bounds the selected workload |
|
|
28
|
+
| `maxSourceBytes` | Bounds each source; it never truncates its contents |
|
|
29
|
+
|
|
30
|
+
Document and byte limits still apply before hydration, including when every artifact is cached.
|
|
31
|
+
Changing a limit does not waive it. Model/reasoning settings, prompt/schema/extractor/adapter/
|
|
32
|
+
ontology identities, confidence/unresolved policy, authentication descriptors, source identity
|
|
33
|
+
and bytes continue to invalidate reuse through their existing artifact keys and/or partition.
|
|
34
|
+
Unknown configuration fields remain identity inputs. Credential environment values are not read
|
|
35
|
+
or added to the cache partition.
|
|
36
|
+
|
|
37
|
+
The new user-cache layout is `~/.cache/context-graph/semantic-artifacts-v1/<partition>/artifacts/semantic/`.
|
|
38
|
+
The partition uses a full SHA-256 digest. Different checkouts, repository IDs, namespaces and
|
|
39
|
+
non-operational configurations remain separate. Workspace storage retains its existing paths
|
|
40
|
+
and behavior; it already allowed built-in Codex reuse after these operational changes. Modules
|
|
41
|
+
retain their full semantic-config identity and their existing storage profiles. Sharing a custom
|
|
42
|
+
workspace storage directory remains an explicit operator choice, not a new global cache feature.
|
|
43
|
+
|
|
44
|
+
Artifact keys and output digests retain the 0.2.1/0.3.0 contract, including the repository-document
|
|
45
|
+
identity policy introduced in 0.2.1. No incompatible 0.2.0 artifact is reinterpreted as current.
|
|
46
|
+
Source approval and assertion review remain distinct; this change never approves an assertion.
|
|
47
|
+
|
|
48
|
+
## Legacy recovery without model work
|
|
49
|
+
|
|
50
|
+
The planner and hydrator use the same store factory. It may read the exact legacy user profile
|
|
51
|
+
derived from the current full configuration after checking the new primary store. A corrupt or
|
|
52
|
+
unreadable primary entry stops reuse; a valid legacy entry does not mask it. Planning remains
|
|
53
|
+
read-only. Hydration can atomically retain a validated legacy artifact in the new partition, then
|
|
54
|
+
revalidate the retained entry to handle concurrent winners. Original files, artifact identities,
|
|
55
|
+
source provenance and raw provider output remain intact.
|
|
56
|
+
|
|
57
|
+
Legacy profile directories contain no reliable checkout/configuration ownership manifest, and an
|
|
58
|
+
interrupted run may have no snapshot. Consequently, the toolkit never scans sibling profiles for
|
|
59
|
+
a matching key. If settings were already changed, the old location cannot safely be inferred.
|
|
60
|
+
|
|
61
|
+
The explicit `cache adopt --from-config <previous.json>` command handles that case without
|
|
62
|
+
providers, authentication probes, indexing, ontology compilation, adapters or snapshot writes.
|
|
63
|
+
Both configurations must resolve to the same checkout and have the same non-operational
|
|
64
|
+
partition identity. Keep the exact old configuration in its original configuration directory.
|
|
65
|
+
Relocation is safe only when its original, unchanged `repositoryRoot` still resolves to the same
|
|
66
|
+
checkout (for example, an absolute root). Do not edit that field to compensate for relocation:
|
|
67
|
+
it is part of the old profile identity. Preserve the original field values and omissions.
|
|
68
|
+
|
|
69
|
+
Adoption resolves a committed ref and uses the same configured/agent selection as the hydration
|
|
70
|
+
preview. It looks up only expected keys in the exact old profile, checks the complete artifact
|
|
71
|
+
contract and output digest, and copies valid candidates atomically. `--dry-run` performs no writes.
|
|
72
|
+
Reports identify reused/adopted/planned/missing/corrupt/unavailable/conflicting entries, configured
|
|
73
|
+
budgets and scope omissions. They omit source bodies, raw provider output and configuration values.
|
|
74
|
+
|
|
75
|
+
Recovery is per artifact: valid copies can complete even if another entry is corrupt or conflicts.
|
|
76
|
+
The report records partial progress and exits unsuccessfully for problems or incomplete scope.
|
|
77
|
+
Missing artifacts are ordinary future extraction work. Corrupt complete artifacts are retained
|
|
78
|
+
for diagnosis; they require an explicit forced refresh or operator repair, not automatic overwrite.
|
|
79
|
+
Incomplete temporary files are not published artifacts and do not satisfy a cache lookup.
|
|
80
|
+
|
|
81
|
+
Concurrent writers use immutable publication. Different outputs under one key are reported as
|
|
82
|
+
conflicts; a matching `outputDigest` field alone is insufficient to accept malformed metadata.
|
|
83
|
+
Explicit forced extraction still creates a separate revision and preserves existing artifacts.
|
|
84
|
+
|
|
85
|
+
## Freshness and limits
|
|
86
|
+
|
|
87
|
+
Semantic reuse does not reuse a snapshot's validation result, skip SCIP or SHACL, or let old
|
|
88
|
+
configuration evidence satisfy current validation. A timeout change still changes the snapshot's
|
|
89
|
+
full configuration digest and user storage profile. Saved evidence remains available under its
|
|
90
|
+
original configuration; this increment does not consolidate historical snapshot directories.
|
|
91
|
+
|
|
92
|
+
The current module-provider contract does not promise operational independence. Supporting that
|
|
93
|
+
requires a separate tested contract, not silently dropping fields from a provider's identity.
|
|
94
|
+
Credential rotation under an unchanged environment-variable name is not detected by this cache;
|
|
95
|
+
credentials are neither cache contents nor cache-identity inputs. Existing filesystem access
|
|
96
|
+
controls and the configured storage location remain the security boundary.
|
|
97
|
+
|
|
98
|
+
Validation uses public fixtures, deterministic Codex runners and installed TypeScript indexing.
|
|
99
|
+
Live semantic quality and private consumer acceptance remain separate. See the
|
|
100
|
+
[1.0 plan](RELEASE-1.0-PLAN.md) for timeout diagnostics and document coverage follow-ups.
|