@weftlet/toolkit 0.2.1 → 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 +80 -0
- package/dist/application/agent-context.d.ts +3 -1
- package/dist/application/agent-context.js +15 -6
- 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 +54 -0
- package/dist/application/hydration-plan.js +71 -0
- package/dist/application/hydrator.d.ts +3 -5
- package/dist/application/hydrator.js +30 -61
- 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 +12 -5
- 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 +13 -0
- package/dist/extraction/semantic-cache.js +66 -0
- package/dist/extraction/semantic-diagnostics.d.ts +9 -0
- package/dist/extraction/semantic-diagnostics.js +15 -0
- package/dist/extraction/semantic-identity.d.ts +7 -0
- package/dist/extraction/semantic-identity.js +29 -0
- package/dist/extraction/semantic-sources.d.ts +27 -0
- package/dist/extraction/semantic-sources.js +35 -0
- package/dist/extraction/semantic.d.ts +5 -1
- package/dist/extraction/semantic.js +47 -24
- 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/graph/rdf.d.ts +1 -1
- package/dist/graph/rdf.js +13 -28
- package/dist/graph/shacl-shapes.d.ts +5 -0
- package/dist/graph/shacl-shapes.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/interfaces/cli.js +86 -19
- package/dist/repository/git.d.ts +1 -1
- package/dist/repository/git.js +5 -1
- 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 +91 -0
- package/docs/ADR-017-shacl-validation-union.md +102 -0
- 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 +148 -32
- package/docs/RELEASING.md +19 -6
- package/docs/ROADMAP.md +46 -5
- package/docs/VALIDATION.md +341 -3
- package/docs/WEFTLET-2.0-PLAN.md +9 -8
- package/docs/extraction-manifest.json +55 -23
- package/package.json +3 -2
|
@@ -2,7 +2,7 @@ import type { ExtractionContext, GraphDiagnostic, GraphFact } from '@weftlet/cor
|
|
|
2
2
|
import type { GitRepository } from '../repository/git.js';
|
|
3
3
|
export declare const routingExtractorIdentity: {
|
|
4
4
|
readonly id: "routing-declaration-extractor";
|
|
5
|
-
readonly version: "1.
|
|
5
|
+
readonly version: "1.1.0";
|
|
6
6
|
};
|
|
7
7
|
/** Reads explicit mapping declarations, never infers routing from matching topic text. */
|
|
8
8
|
export declare function extractRoutingDeclarations(repository: GitRepository, context: ExtractionContext, paths: readonly string[], options?: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
|
+
import { repositoryFileNode as fileNode } from '@weftlet/core';
|
|
2
3
|
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
3
|
-
export const routingExtractorIdentity = { id: 'routing-declaration-extractor', version: '1.
|
|
4
|
+
export const routingExtractorIdentity = { id: 'routing-declaration-extractor', version: '1.1.0' };
|
|
4
5
|
function record(value) {
|
|
5
6
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
7
|
}
|
|
@@ -61,10 +62,6 @@ function parseRoutes(content, path) {
|
|
|
61
62
|
producer: route.producer, consumer: route.consumer };
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
|
-
function fileNode(path) {
|
|
65
|
-
return { id: `component:file:${path}`, label: path,
|
|
66
|
-
kind: /\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component' };
|
|
67
|
-
}
|
|
68
65
|
/** Reads explicit mapping declarations, never infers routing from matching topic text. */
|
|
69
66
|
export async function extractRoutingDeclarations(repository, context, paths, options = {}) {
|
|
70
67
|
if (paths.length > 16 || new Set(paths).size !== paths.length ||
|
|
@@ -96,7 +93,7 @@ export async function extractRoutingDeclarations(repository, context, paths, opt
|
|
|
96
93
|
if (!tracked.has(path)) {
|
|
97
94
|
if (!options.allowMissingSources)
|
|
98
95
|
throw new Error(`Routing source ${path} is not a committed file at ${context.commitSha}`);
|
|
99
|
-
addDiagnostic(`Routing source ${path} is absent from this draft; no bindings from that source were retained.`, [], [
|
|
96
|
+
addDiagnostic(`Routing source ${path} is absent from this draft; no bindings from that source were retained.`, [], [fileNode(path).id]);
|
|
100
97
|
continue;
|
|
101
98
|
}
|
|
102
99
|
if (!regular.has(path))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SemanticArtifact, SemanticSource } from '@weftlet/core';
|
|
2
|
+
import type { ContextGraphConfig } from '../configuration/config.js';
|
|
3
|
+
import type { SemanticArtifactStore } from '../storage/storage.js';
|
|
4
|
+
export type SemanticCacheStatus = 'hit' | 'miss' | 'forced' | 'corrupt' | 'unavailable';
|
|
5
|
+
export interface SemanticCacheInspection {
|
|
6
|
+
readonly status: SemanticCacheStatus;
|
|
7
|
+
readonly key?: string;
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
readonly artifact?: SemanticArtifact;
|
|
10
|
+
readonly origin?: 'primary' | 'legacy';
|
|
11
|
+
}
|
|
12
|
+
/** Read-only; a broken artifact is not an extraction miss or permission to overwrite evidence. */
|
|
13
|
+
export declare function inspectSemanticCache(store: SemanticArtifactStore, config: ContextGraphConfig['semantic'], source: SemanticSource, modelIdentity: string, forceRevision?: string): Promise<SemanticCacheInspection>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { semanticArtifactKey, semanticArtifactOutputDigest } from './semantic-identity.js';
|
|
2
|
+
function record(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function strings(value) {
|
|
6
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
7
|
+
}
|
|
8
|
+
function matchesIdentity(value, key, source, config, modelIdentity) {
|
|
9
|
+
return value.key === key && value.stableSemanticId === source.stableSemanticId &&
|
|
10
|
+
value.sourceDigest === source.digest && value.modelIdentity === modelIdentity &&
|
|
11
|
+
['promptVersion', 'extractorVersion', 'adapterVersion', 'ontologyVersion'].every((field) => value[field] === config[field]);
|
|
12
|
+
}
|
|
13
|
+
function reusableArtifact(value, key, source, config, modelIdentity) {
|
|
14
|
+
if (!record(value) || !matchesIdentity(value, key, source, config, modelIdentity) ||
|
|
15
|
+
!record(value.source) || value.source.digest !== source.digest ||
|
|
16
|
+
typeof value.source.path !== 'string' || typeof value.source.commitSha !== 'string' ||
|
|
17
|
+
!Array.isArray(value.facts) || !Array.isArray(value.diagnostics) || !strings(value.unresolvedMappings) ||
|
|
18
|
+
typeof value.oml !== 'string' || typeof value.rawModelOutput !== 'string' ||
|
|
19
|
+
typeof value.outputDigest !== 'string')
|
|
20
|
+
return false;
|
|
21
|
+
const lineCount = source.content.split('\n').length;
|
|
22
|
+
const span = (item) => record(item) && typeof item.path === 'string' &&
|
|
23
|
+
item.digest === source.digest && typeof item.commitSha === 'string' &&
|
|
24
|
+
Number.isInteger(item.startLine) && Number.isInteger(item.endLine) &&
|
|
25
|
+
Number(item.startLine) >= 1 && Number(item.endLine) >= Number(item.startLine) && Number(item.endLine) <= lineCount;
|
|
26
|
+
const node = (item) => record(item) && typeof item.id === 'string' && item.id.length > 0 &&
|
|
27
|
+
typeof item.kind === 'string' && typeof item.label === 'string';
|
|
28
|
+
if (!span(value.source) || !value.facts.every((fact) => record(fact) && typeof fact.id === 'string' &&
|
|
29
|
+
node(fact.subject) && node(fact.object) && typeof fact.predicate === 'string' && span(fact.provenance) &&
|
|
30
|
+
record(fact.extractor) && fact.extractor.id === modelIdentity && fact.extractor.version === config.extractorVersion &&
|
|
31
|
+
typeof fact.confidence === 'number' && fact.confidence >= config.minimumConfidence && fact.confidence <= 1))
|
|
32
|
+
return false;
|
|
33
|
+
if (!value.diagnostics.every((item) => record(item) && typeof item.id === 'string' &&
|
|
34
|
+
typeof item.kind === 'string' && typeof item.reason === 'string' && strings(item.relatedNodeIds) &&
|
|
35
|
+
Array.isArray(item.provenance) && item.provenance.length > 0 && item.provenance.every(span)))
|
|
36
|
+
return false;
|
|
37
|
+
if (config.failOnUnresolved && (value.unresolvedMappings.length || value.diagnostics.length))
|
|
38
|
+
return false;
|
|
39
|
+
return semanticArtifactOutputDigest(value) === value.outputDigest;
|
|
40
|
+
}
|
|
41
|
+
/** Read-only; a broken artifact is not an extraction miss or permission to overwrite evidence. */
|
|
42
|
+
export async function inspectSemanticCache(store, config, source, modelIdentity, forceRevision) {
|
|
43
|
+
const key = semanticArtifactKey(config, source, modelIdentity, forceRevision);
|
|
44
|
+
let value;
|
|
45
|
+
let origin;
|
|
46
|
+
try {
|
|
47
|
+
const entry = await store.readEntry(key);
|
|
48
|
+
value = entry?.artifact;
|
|
49
|
+
origin = entry?.origin;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error instanceof SyntaxError
|
|
53
|
+
? { status: 'corrupt', key, reason: 'Cached artifact is not valid JSON; retain it for diagnosis or use an explicit forced refresh.' }
|
|
54
|
+
: { status: 'unavailable', key, reason: 'Cached artifact could not be read; check local storage access before hydrating.' };
|
|
55
|
+
}
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return { status: 'miss', key, reason: 'No artifact exists for the current source and extraction contract.' };
|
|
58
|
+
if (!reusableArtifact(value, key, source, config, modelIdentity)) {
|
|
59
|
+
return { status: 'corrupt', key, reason: 'Cached artifact identity, structure or content digest is invalid; retain it for diagnosis or use an explicit forced refresh.' };
|
|
60
|
+
}
|
|
61
|
+
return { status: 'hit', key, origin,
|
|
62
|
+
reason: origin === 'legacy'
|
|
63
|
+
? 'Validated artifact is available in the exact legacy configuration profile; hydration can retain a copy in the operationally stable cache.'
|
|
64
|
+
: 'Readable artifact matches the source and extraction contract; reuse remains subject to unchanged inputs.',
|
|
65
|
+
artifact: value };
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SemanticSource } from '@weftlet/core';
|
|
2
|
+
export type SemanticHydrationPhase = 'semantic-cache-read' | 'semantic-extract' | 'semantic-validate' | 'semantic-cache-write';
|
|
3
|
+
/** Preserve policy distinctions without copying source-specific messages into run reports. */
|
|
4
|
+
export declare class SemanticPolicyError extends Error {
|
|
5
|
+
readonly code: 'unresolved' | 'low-confidence';
|
|
6
|
+
constructor(code: 'unresolved' | 'low-confidence', message: string);
|
|
7
|
+
}
|
|
8
|
+
/** Correlation only: never used as an artifact key or an authorization boundary. */
|
|
9
|
+
export declare function semanticSourceId(repositoryId: string, source: Pick<SemanticSource, 'stableSemanticId'>): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
2
|
+
/** Preserve policy distinctions without copying source-specific messages into run reports. */
|
|
3
|
+
export class SemanticPolicyError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'SemanticPolicyError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Correlation only: never used as an artifact key or an authorization boundary. */
|
|
12
|
+
export function semanticSourceId(repositoryId, source) {
|
|
13
|
+
return `source-${sha256(stableJson({ kind: 'weftlet-semantic-source-v1', repositoryId,
|
|
14
|
+
stableSemanticId: source.stableSemanticId }))}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SemanticArtifact, SemanticSource } from '@weftlet/core';
|
|
2
|
+
import type { ContextGraphConfig } from '../configuration/config.js';
|
|
3
|
+
/** Pure identity: inspecting planned work must not load providers or probe credentials. */
|
|
4
|
+
export declare function semanticProviderIdentity(config: ContextGraphConfig['semantic']): string;
|
|
5
|
+
/** Preserve the 0.2.1 artifact contract, including identity-policy invalidation. */
|
|
6
|
+
export declare function semanticArtifactKey(config: ContextGraphConfig['semantic'], source: SemanticSource, modelIdentity: string, forceRevision?: string): string;
|
|
7
|
+
export declare function semanticArtifactOutputDigest(artifact: Pick<SemanticArtifact, 'facts' | 'diagnostics' | 'oml' | 'rawModelOutput'>): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { sha256, stableJson } from '../infrastructure/util.js';
|
|
2
|
+
import { repositoryNodeIdentityVersion } from './repository-nodes.js';
|
|
3
|
+
/** Pure identity: inspecting planned work must not load providers or probe credentials. */
|
|
4
|
+
export function semanticProviderIdentity(config) {
|
|
5
|
+
return config.provider === 'codex-cli'
|
|
6
|
+
? `codex-cli:${config.model}:${config.reasoningEffort}:${config.promptVersion}:contract-v3`
|
|
7
|
+
: `${config.providerIdentity}:${sha256(stableJson(config))}:contract-v2`;
|
|
8
|
+
}
|
|
9
|
+
/** Preserve the 0.2.1 artifact contract, including identity-policy invalidation. */
|
|
10
|
+
export function semanticArtifactKey(config, source, modelIdentity, forceRevision) {
|
|
11
|
+
return sha256(stableJson({
|
|
12
|
+
stableSemanticId: source.stableSemanticId,
|
|
13
|
+
sourceDigest: source.digest,
|
|
14
|
+
promptVersion: config.promptVersion,
|
|
15
|
+
extractorVersion: config.extractorVersion,
|
|
16
|
+
modelIdentity,
|
|
17
|
+
adapterVersion: config.adapterVersion,
|
|
18
|
+
ontologyVersion: config.ontologyVersion,
|
|
19
|
+
minimumConfidence: config.minimumConfidence,
|
|
20
|
+
failOnUnresolved: config.failOnUnresolved,
|
|
21
|
+
forceRevision,
|
|
22
|
+
diagnosticContractVersion: 3,
|
|
23
|
+
repositoryNodeIdentityVersion,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export function semanticArtifactOutputDigest(artifact) {
|
|
27
|
+
return sha256(stableJson({ facts: artifact.facts, diagnostics: artifact.diagnostics ?? [],
|
|
28
|
+
compiledOml: artifact.oml, rawModelOutput: artifact.rawModelOutput }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SemanticSource } from '@weftlet/core';
|
|
2
|
+
import type { ContextGraphConfig } from '../configuration/config.js';
|
|
3
|
+
import type { GitRepository } from '../repository/git.js';
|
|
4
|
+
export interface SemanticSelection {
|
|
5
|
+
readonly selector: string;
|
|
6
|
+
readonly paths: readonly string[];
|
|
7
|
+
readonly excludedPaths: readonly string[];
|
|
8
|
+
readonly status: 'matched' | 'no-documents' | 'missing';
|
|
9
|
+
}
|
|
10
|
+
export interface SemanticInputIssue {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly severity: 'error' | 'warning';
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly paths: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/** Git path selection, source bytes and limits are shared by preview and execution. */
|
|
17
|
+
export declare function selectSemanticInputs(repository: GitRepository, commitSha: string, selectors: readonly string[], config: ContextGraphConfig['semantic']): Promise<{
|
|
18
|
+
sources: readonly SemanticSource[];
|
|
19
|
+
selections: SemanticSelection[];
|
|
20
|
+
budgets: {
|
|
21
|
+
maxDocuments: number;
|
|
22
|
+
maxSourceBytes: number;
|
|
23
|
+
selectedDocuments: number;
|
|
24
|
+
totalSourceBytes: number;
|
|
25
|
+
};
|
|
26
|
+
issues: SemanticInputIssue[];
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Git path selection, source bytes and limits are shared by preview and execution. */
|
|
2
|
+
export async function selectSemanticInputs(repository, commitSha, selectors, config) {
|
|
3
|
+
const selections = [];
|
|
4
|
+
for (const selector of [...new Set(selectors)]) {
|
|
5
|
+
const files = await repository.listFilesAt(commitSha, [selector]);
|
|
6
|
+
const paths = files.filter((path) => /\.(?:md|mdx|txt)$/i.test(path));
|
|
7
|
+
selections.push({ selector, paths, excludedPaths: files.filter((path) => !paths.includes(path)),
|
|
8
|
+
status: paths.length ? 'matched' : files.length ? 'no-documents' : 'missing' });
|
|
9
|
+
}
|
|
10
|
+
const sources = await repository.semanticSources({ commitSha }, selectors);
|
|
11
|
+
const budgets = { maxDocuments: config.maxDocuments ?? 8, maxSourceBytes: config.maxSourceBytes ?? 65_536,
|
|
12
|
+
selectedDocuments: sources.length, totalSourceBytes: sources.reduce((sum, source) => sum + Buffer.byteLength(source.content), 0) };
|
|
13
|
+
const issues = selections.filter((item) => item.status !== 'matched').map((item) => ({
|
|
14
|
+
code: item.status === 'missing' ? 'missing-source' : 'unsupported-source', severity: 'warning',
|
|
15
|
+
message: item.status === 'missing' ? `Semantic selector ${item.selector} matches no committed files.`
|
|
16
|
+
: `Semantic selector ${item.selector} matches no supported Markdown/text documents.`, paths: [item.selector],
|
|
17
|
+
}));
|
|
18
|
+
if (sources.length > budgets.maxDocuments)
|
|
19
|
+
issues.push({ code: 'document-budget', severity: 'error',
|
|
20
|
+
message: `Semantic source budget exceeded: ${sources.length} documents exceed maxDocuments ${budgets.maxDocuments}.`,
|
|
21
|
+
paths: sources.map((source) => source.path) });
|
|
22
|
+
const oversized = sources.filter((source) => Buffer.byteLength(source.content) > budgets.maxSourceBytes);
|
|
23
|
+
if (oversized.length)
|
|
24
|
+
issues.push({ code: 'source-byte-budget', severity: 'error',
|
|
25
|
+
message: `Semantic source budget exceeded: ${oversized.length} documents exceed the per-document maxSourceBytes ${budgets.maxSourceBytes}.`,
|
|
26
|
+
paths: oversized.map((source) => source.path) });
|
|
27
|
+
const byId = new Map();
|
|
28
|
+
for (const source of sources)
|
|
29
|
+
byId.set(source.stableSemanticId, [...(byId.get(source.stableSemanticId) ?? []), source.path]);
|
|
30
|
+
for (const [id, paths] of byId)
|
|
31
|
+
if (paths.length > 1)
|
|
32
|
+
issues.push({ code: 'duplicate-semantic-id', severity: 'error',
|
|
33
|
+
message: `Duplicate stable semantic ID: ${id}`, paths });
|
|
34
|
+
return { sources, selections, budgets, issues };
|
|
35
|
+
}
|
|
@@ -7,6 +7,7 @@ export type { SemanticRunOptions } from "./semantic-process.js";
|
|
|
7
7
|
import type { ExtractionContext, GraphNodeKind, GraphPredicate, SemanticArtifact, SemanticSource, DiagnosticKind } from "@weftlet/core";
|
|
8
8
|
import type { ContextGraphConfig } from "../configuration/config.js";
|
|
9
9
|
import type { SemanticArtifactStore } from "../storage/storage.js";
|
|
10
|
+
import { type SemanticHydrationPhase } from './semantic-diagnostics.js';
|
|
10
11
|
export interface ExtractedAssertion {
|
|
11
12
|
readonly subject: {
|
|
12
13
|
readonly id: string;
|
|
@@ -66,8 +67,11 @@ export declare class SemanticHydrator {
|
|
|
66
67
|
private readonly store;
|
|
67
68
|
private readonly extractor;
|
|
68
69
|
constructor(config: ContextGraphConfig['semantic'], store: SemanticArtifactStore, extractor: SemanticExtractor);
|
|
69
|
-
hydrate(source: SemanticSource, context: ExtractionContext, forceRevision?: string, options?: SemanticRunOptions
|
|
70
|
+
hydrate(source: SemanticSource, context: ExtractionContext, forceRevision?: string, options?: SemanticRunOptions & {
|
|
71
|
+
readonly onPhase?: (phase: SemanticHydrationPhase) => void;
|
|
72
|
+
}): Promise<{
|
|
70
73
|
artifact: SemanticArtifact;
|
|
71
74
|
reused: boolean;
|
|
72
75
|
}>;
|
|
76
|
+
private promoteLegacy;
|
|
73
77
|
}
|
|
@@ -8,7 +8,10 @@ import { resolveCodexInvocation } from './codex-command.js';
|
|
|
8
8
|
import { runSemanticProcess, SemanticProviderError } from "./semantic-process.js";
|
|
9
9
|
export { SemanticProviderError } from "./semantic-process.js";
|
|
10
10
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
11
|
-
import { canonicalizeRepositoryDocumentNode
|
|
11
|
+
import { canonicalizeRepositoryDocumentNode } from './repository-nodes.js';
|
|
12
|
+
import { semanticArtifactKey, semanticArtifactOutputDigest, semanticProviderIdentity } from './semantic-identity.js';
|
|
13
|
+
import { inspectSemanticCache } from './semantic-cache.js';
|
|
14
|
+
import { SemanticPolicyError } from './semantic-diagnostics.js';
|
|
12
15
|
import { assertSemanticCitation, numberedSemanticSource, semanticCitationSchema, } from "./semantic-citations.js";
|
|
13
16
|
function canonicalizeRepositoryDocuments(output) {
|
|
14
17
|
const ids = new Map();
|
|
@@ -228,7 +231,7 @@ export class CodexCliSemanticExtractor {
|
|
|
228
231
|
this.config = config;
|
|
229
232
|
this.runner = runner;
|
|
230
233
|
this.loginProbe = loginProbe;
|
|
231
|
-
this.identity =
|
|
234
|
+
this.identity = semanticProviderIdentity(config);
|
|
232
235
|
}
|
|
233
236
|
async extract(source, context, options = {}) {
|
|
234
237
|
options.signal?.throwIfAborted();
|
|
@@ -332,26 +335,32 @@ export class SemanticHydrator {
|
|
|
332
335
|
this.extractor = extractor;
|
|
333
336
|
}
|
|
334
337
|
async hydrate(source, context, forceRevision, options = {}) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
sourceDigest: source.digest,
|
|
339
|
-
promptVersion: this.config.promptVersion,
|
|
340
|
-
extractorVersion: this.config.extractorVersion,
|
|
341
|
-
modelIdentity: this.extractor.identity,
|
|
342
|
-
adapterVersion: this.config.adapterVersion,
|
|
343
|
-
ontologyVersion: this.config.ontologyVersion,
|
|
344
|
-
minimumConfidence: this.config.minimumConfidence,
|
|
345
|
-
failOnUnresolved: this.config.failOnUnresolved,
|
|
346
|
-
forceRevision,
|
|
347
|
-
diagnosticContractVersion: 3,
|
|
348
|
-
repositoryNodeIdentityVersion,
|
|
338
|
+
const checkpoint = () => {
|
|
339
|
+
if (options.signal?.aborted)
|
|
340
|
+
throw new SemanticProviderError('cancelled', 'Semantic extraction cancelled');
|
|
349
341
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
342
|
+
checkpoint();
|
|
343
|
+
options.onPhase?.('semantic-cache-read');
|
|
344
|
+
const key = semanticArtifactKey(this.config, source, this.extractor.identity, forceRevision);
|
|
345
|
+
const cached = await inspectSemanticCache(this.store, this.config, source, this.extractor.identity, forceRevision);
|
|
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
|
+
}
|
|
356
|
+
if (cached.status !== 'miss')
|
|
357
|
+
throw new Error(cached.reason);
|
|
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');
|
|
355
364
|
assertSemanticOutput(extraction.parsed, source);
|
|
356
365
|
if (typeof extraction.rawModelOutput !== 'string' || Buffer.byteLength(extraction.rawModelOutput) > 2 * 1024 * 1024) {
|
|
357
366
|
throw new SemanticProviderError('invalid-output', 'Invalid or oversized raw semantic output');
|
|
@@ -361,7 +370,7 @@ export class SemanticHydrator {
|
|
|
361
370
|
const facts = output.assertions.map((assertion, index) => {
|
|
362
371
|
assertSemanticCitation(assertion.citation, source.path, lineCount, `assertion[${index}]`);
|
|
363
372
|
if (assertion.confidence < this.config.minimumConfidence) {
|
|
364
|
-
throw new
|
|
373
|
+
throw new SemanticPolicyError('low-confidence', `Low-confidence semantic assertion for ${source.path}`);
|
|
365
374
|
}
|
|
366
375
|
const provenance = {
|
|
367
376
|
path: source.path,
|
|
@@ -408,10 +417,10 @@ export class SemanticHydrator {
|
|
|
408
417
|
});
|
|
409
418
|
const unresolvedMappings = output.unresolvedMappings ?? [];
|
|
410
419
|
if (this.config.failOnUnresolved && (unresolvedMappings.length > 0 || diagnostics.length > 0)) {
|
|
411
|
-
throw new
|
|
420
|
+
throw new SemanticPolicyError('unresolved', `Unresolved semantic mappings for ${source.path}: ${[...unresolvedMappings, ...diagnostics.map((item) => item.reason)].join('; ')}`);
|
|
412
421
|
}
|
|
413
422
|
const compiledOml = renderOml(key, output.assertions);
|
|
414
|
-
const outputDigest =
|
|
423
|
+
const outputDigest = semanticArtifactOutputDigest({ facts, diagnostics, oml: compiledOml, rawModelOutput: extraction.rawModelOutput });
|
|
415
424
|
const artifact = {
|
|
416
425
|
key,
|
|
417
426
|
stableSemanticId: source.stableSemanticId,
|
|
@@ -435,7 +444,21 @@ export class SemanticHydrator {
|
|
|
435
444
|
rawModelOutput: extraction.rawModelOutput,
|
|
436
445
|
outputDigest,
|
|
437
446
|
};
|
|
447
|
+
checkpoint();
|
|
448
|
+
options.onPhase?.('semantic-cache-write');
|
|
438
449
|
await this.store.write(artifact);
|
|
439
450
|
return { artifact, reused: false };
|
|
440
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
|
+
}
|
|
441
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/graph/rdf.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphFact, GraphSnapshot } from "@weftlet/core";
|
|
2
|
-
export
|
|
2
|
+
export { CG } from './shacl-shapes.js';
|
|
3
3
|
export declare const nodeIri: (id: string) => string;
|
|
4
4
|
export declare const factIri: (id: string) => string;
|
|
5
5
|
/** Lossless RDF dataset: one named graph per cited assertion, with provenance in the default graph. */
|