@weftlet/toolkit 0.3.0 → 0.5.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 +26 -1
- 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 +5 -5
- package/dist/application/hydrator.js +37 -52
- package/dist/application/overlay.d.ts +3 -1
- package/dist/application/overlay.js +13 -4
- 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 +32 -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/configuration/config.js +1 -1
- 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/identity-preflight.d.ts +57 -0
- package/dist/graph/identity-preflight.js +71 -0
- package/dist/graph/query.js +2 -0
- package/dist/graph/rdf.d.ts +4 -1
- package/dist/graph/rdf.js +10 -6
- package/dist/graph/validation-progress.d.ts +14 -0
- package/dist/graph/validation-progress.js +18 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/interfaces/cli.js +52 -22
- 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/ADR-021-identity-preflight-and-validation-timing.md +81 -0
- package/docs/PROVIDERS.md +58 -1
- package/docs/RELEASE-1.0-PLAN.md +100 -34
- package/docs/RELEASING.md +4 -2
- package/docs/ROADMAP.md +40 -15
- package/docs/VALIDATION.md +351 -4
- package/docs/extraction-manifest.json +50 -23
- package/package.json +2 -2
|
@@ -1,20 +1,14 @@
|
|
|
1
|
+
import { repositoryFileNode } from '@weftlet/core';
|
|
2
|
+
export { repositoryFileNode } from '@weftlet/core';
|
|
1
3
|
/** Internal cache contract: changing this policy must also change snapshot assembly identity. */
|
|
2
4
|
export const repositoryNodeIdentityVersion = 1;
|
|
3
|
-
function isDocumentPath(path) {
|
|
4
|
-
return /\.(?:md|mdx|txt)$/i.test(path);
|
|
5
|
-
}
|
|
6
|
-
/** Physical file identity; this does not establish existence, intent, ownership or approval. */
|
|
7
|
-
export function repositoryFileNode(path) {
|
|
8
|
-
if (isDocumentPath(path))
|
|
9
|
-
return { id: `document:${path}`, kind: 'Document', label: path };
|
|
10
|
-
return { id: `component:file:${path}`, label: path,
|
|
11
|
-
kind: /\.(spec|test)\.[^.]+$/i.test(path) ? 'Evidence' : 'Component' };
|
|
12
|
-
}
|
|
13
5
|
/** Normalize only reserved document/file identities, never inferred business concept IDs. */
|
|
14
6
|
export function canonicalizeRepositoryDocumentNode(node) {
|
|
15
7
|
const match = /^(?:component:file:|document:)(.+)$/.exec(node.id);
|
|
16
|
-
if (!match
|
|
8
|
+
if (!match)
|
|
17
9
|
return node;
|
|
18
10
|
const canonical = repositoryFileNode(match[1]);
|
|
11
|
+
if (canonical.kind !== 'Document')
|
|
12
|
+
return node;
|
|
19
13
|
return { ...node, id: canonical.id, kind: canonical.kind };
|
|
20
14
|
}
|
|
@@ -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))
|
|
@@ -7,6 +7,7 @@ export interface SemanticCacheInspection {
|
|
|
7
7
|
readonly key?: string;
|
|
8
8
|
readonly reason: string;
|
|
9
9
|
readonly artifact?: SemanticArtifact;
|
|
10
|
+
readonly origin?: 'primary' | 'legacy';
|
|
10
11
|
}
|
|
11
12
|
/** Read-only; a broken artifact is not an extraction miss or permission to overwrite evidence. */
|
|
12
13
|
export declare function inspectSemanticCache(store: SemanticArtifactStore, config: ContextGraphConfig['semantic'], source: SemanticSource, modelIdentity: string, forceRevision?: string): Promise<SemanticCacheInspection>;
|
|
@@ -42,8 +42,11 @@ function reusableArtifact(value, key, source, config, modelIdentity) {
|
|
|
42
42
|
export async function inspectSemanticCache(store, config, source, modelIdentity, forceRevision) {
|
|
43
43
|
const key = semanticArtifactKey(config, source, modelIdentity, forceRevision);
|
|
44
44
|
let value;
|
|
45
|
+
let origin;
|
|
45
46
|
try {
|
|
46
|
-
|
|
47
|
+
const entry = await store.readEntry(key);
|
|
48
|
+
value = entry?.artifact;
|
|
49
|
+
origin = entry?.origin;
|
|
47
50
|
}
|
|
48
51
|
catch (error) {
|
|
49
52
|
return error instanceof SyntaxError
|
|
@@ -55,5 +58,9 @@ export async function inspectSemanticCache(store, config, source, modelIdentity,
|
|
|
55
58
|
if (!reusableArtifact(value, key, source, config, modelIdentity)) {
|
|
56
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.' };
|
|
57
60
|
}
|
|
58
|
-
return { status: 'hit', key,
|
|
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 };
|
|
59
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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ExtractorIdentity, type GraphFact, type GraphNodeKind, type SourceSpan } from '@weftlet/core';
|
|
2
|
+
/** Diagnostic detail is an invocation option, not extraction policy or a cache identity input. */
|
|
3
|
+
export interface GraphIdentityPreflightOptions {
|
|
4
|
+
readonly repositoryId: string;
|
|
5
|
+
readonly includeDetails?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type ReportedIdentityKind = GraphNodeKind | `unknown-kind-${string}`;
|
|
8
|
+
export interface GraphIdentityOccurrenceDetails {
|
|
9
|
+
readonly nodeId: string;
|
|
10
|
+
readonly nodeKind: GraphNodeKind;
|
|
11
|
+
readonly factId: string;
|
|
12
|
+
readonly extractor: ExtractorIdentity;
|
|
13
|
+
readonly provenance: SourceSpan;
|
|
14
|
+
readonly confidence: number;
|
|
15
|
+
readonly authority?: GraphFact['authority'];
|
|
16
|
+
readonly sourceAuthority?: GraphFact['sourceAuthority'];
|
|
17
|
+
readonly extractionReview?: GraphFact['extractionReview'];
|
|
18
|
+
}
|
|
19
|
+
export interface GraphIdentityOccurrence {
|
|
20
|
+
readonly factIndex: number;
|
|
21
|
+
readonly side: 'subject' | 'object';
|
|
22
|
+
readonly kind: ReportedIdentityKind;
|
|
23
|
+
readonly factRef: string;
|
|
24
|
+
readonly sourceRef: string;
|
|
25
|
+
readonly extractorRef: string;
|
|
26
|
+
readonly details?: GraphIdentityOccurrenceDetails;
|
|
27
|
+
}
|
|
28
|
+
export interface GraphIdentityConflict {
|
|
29
|
+
readonly nodeRef: string;
|
|
30
|
+
readonly kinds: readonly ReportedIdentityKind[];
|
|
31
|
+
readonly occurrences: readonly GraphIdentityOccurrence[];
|
|
32
|
+
readonly details?: {
|
|
33
|
+
readonly nodeId: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface GraphIdentityPreflightReport {
|
|
37
|
+
readonly schemaVersion: 1;
|
|
38
|
+
readonly kind: 'graph-identity-preflight';
|
|
39
|
+
readonly category: 'extraction-gap';
|
|
40
|
+
readonly phase: 'validate-identities';
|
|
41
|
+
readonly passed: boolean;
|
|
42
|
+
/** A passing identity preflight never establishes full graph conformance. */
|
|
43
|
+
readonly shaclValidated: false;
|
|
44
|
+
readonly factCount: number;
|
|
45
|
+
readonly nodeCount: number;
|
|
46
|
+
readonly conflictCount: number;
|
|
47
|
+
readonly conflicts: readonly GraphIdentityConflict[];
|
|
48
|
+
}
|
|
49
|
+
/** Find the single-kind constraint's conflicts without constructing RDF or changing any fact. */
|
|
50
|
+
export declare function inspectGraphIdentities(facts: readonly GraphFact[], options: GraphIdentityPreflightOptions): GraphIdentityPreflightReport;
|
|
51
|
+
/** The message is safe even when the caller explicitly requested local report details. */
|
|
52
|
+
export declare class GraphIdentityConflictError extends Error {
|
|
53
|
+
readonly report: GraphIdentityPreflightReport;
|
|
54
|
+
readonly code = "node-kind-conflict";
|
|
55
|
+
constructor(report: GraphIdentityPreflightReport);
|
|
56
|
+
}
|
|
57
|
+
export declare function assertGraphIdentities(facts: readonly GraphFact[], options: GraphIdentityPreflightOptions): GraphIdentityPreflightReport;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sha256, stableJson } from '@weftlet/core';
|
|
2
|
+
const publicKinds = new Set([
|
|
3
|
+
'Project', 'Document', 'Capability', 'Workflow', 'Component', 'Contract', 'Evidence',
|
|
4
|
+
'Invariant', 'Snapshot', 'Provider', 'PersistenceModel', 'Scope',
|
|
5
|
+
]);
|
|
6
|
+
/** Find the single-kind constraint's conflicts without constructing RDF or changing any fact. */
|
|
7
|
+
export function inspectGraphIdentities(facts, options) {
|
|
8
|
+
const identities = new Map();
|
|
9
|
+
for (const fact of facts)
|
|
10
|
+
for (const node of [fact.subject, fact.object]) {
|
|
11
|
+
let kinds = identities.get(node.id);
|
|
12
|
+
if (!kinds)
|
|
13
|
+
identities.set(node.id, kinds = new Set());
|
|
14
|
+
kinds.add(node.kind);
|
|
15
|
+
}
|
|
16
|
+
const ref = (domain, ...values) => `identity-${domain}-${sha256(stableJson(['graph-identity-preflight-v1', options.repositoryId, domain, ...values]))}`;
|
|
17
|
+
const reportedKind = (kind) => publicKinds.has(kind)
|
|
18
|
+
? kind : `unknown-kind-${sha256(stableJson(['graph-identity-kind-v1', options.repositoryId, kind]))}`;
|
|
19
|
+
const conflicts = new Map();
|
|
20
|
+
for (const [id, kinds] of identities)
|
|
21
|
+
if (kinds.size > 1) {
|
|
22
|
+
conflicts.set(id, { nodeRef: ref('node', id), kinds: [...kinds].map(reportedKind), occurrences: [],
|
|
23
|
+
...(options.includeDetails ? { details: { nodeId: id } } : {}) });
|
|
24
|
+
}
|
|
25
|
+
// The common conflict-free path allocates no evidence reports. Preserve every conflicting
|
|
26
|
+
// occurrence, including duplicate fact IDs and the two ends of a self-reference.
|
|
27
|
+
if (conflicts.size)
|
|
28
|
+
for (const [factIndex, fact] of facts.entries()) {
|
|
29
|
+
const sides = ['subject', 'object'].filter(side => conflicts.has(fact[side].id));
|
|
30
|
+
if (!sides.length)
|
|
31
|
+
continue;
|
|
32
|
+
const references = { factRef: ref('fact', fact.id), sourceRef: ref('source', fact.provenance.path),
|
|
33
|
+
extractorRef: ref('extractor', fact.extractor.id, fact.extractor.version) };
|
|
34
|
+
for (const side of sides) {
|
|
35
|
+
const node = fact[side];
|
|
36
|
+
conflicts.get(node.id).occurrences.push({ factIndex, side, kind: reportedKind(node.kind), ...references,
|
|
37
|
+
...(options.includeDetails ? { details: {
|
|
38
|
+
nodeId: node.id, nodeKind: node.kind, factId: fact.id,
|
|
39
|
+
extractor: { id: fact.extractor.id, version: fact.extractor.version },
|
|
40
|
+
provenance: { path: fact.provenance.path, startLine: fact.provenance.startLine,
|
|
41
|
+
endLine: fact.provenance.endLine, digest: fact.provenance.digest, commitSha: fact.provenance.commitSha },
|
|
42
|
+
confidence: fact.confidence,
|
|
43
|
+
...(fact.authority === undefined ? {} : { authority: fact.authority }),
|
|
44
|
+
...(fact.sourceAuthority === undefined ? {} : { sourceAuthority: fact.sourceAuthority }),
|
|
45
|
+
...(fact.extractionReview === undefined ? {} : { extractionReview: fact.extractionReview }),
|
|
46
|
+
} } : {}) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { schemaVersion: 1, kind: 'graph-identity-preflight', category: 'extraction-gap',
|
|
50
|
+
phase: 'validate-identities', passed: conflicts.size === 0, shaclValidated: false,
|
|
51
|
+
factCount: facts.length, nodeCount: identities.size, conflictCount: conflicts.size,
|
|
52
|
+
conflicts: [...conflicts.values()] };
|
|
53
|
+
}
|
|
54
|
+
/** The message is safe even when the caller explicitly requested local report details. */
|
|
55
|
+
export class GraphIdentityConflictError extends Error {
|
|
56
|
+
report;
|
|
57
|
+
code = 'node-kind-conflict';
|
|
58
|
+
constructor(report) {
|
|
59
|
+
super(`Graph identity preflight found ${report.conflictCount} node kind conflict(s) across ${report.factCount} facts. ` +
|
|
60
|
+
'Full SHACL validation was not run. Use --validation-details for local node and cited evidence details. ' +
|
|
61
|
+
'Correct the extractor or assertion identity disagreement, then retry; no graph facts were rewritten by this check.');
|
|
62
|
+
this.report = report;
|
|
63
|
+
this.name = 'GraphIdentityConflictError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function assertGraphIdentities(facts, options) {
|
|
67
|
+
const report = inspectGraphIdentities(facts, options);
|
|
68
|
+
if (!report.passed)
|
|
69
|
+
throw new GraphIdentityConflictError(report);
|
|
70
|
+
return report;
|
|
71
|
+
}
|
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,4 +1,5 @@
|
|
|
1
1
|
import type { GraphFact, GraphSnapshot } from "@weftlet/core";
|
|
2
|
+
import { type GraphValidationProgress } from './validation-progress.js';
|
|
2
3
|
export { CG } from './shacl-shapes.js';
|
|
3
4
|
export declare const nodeIri: (id: string) => string;
|
|
4
5
|
export declare const factIri: (id: string) => string;
|
|
@@ -7,7 +8,9 @@ export declare class RdfEvidenceGraph {
|
|
|
7
8
|
readonly snapshot: GraphSnapshot;
|
|
8
9
|
private readonly dataset;
|
|
9
10
|
constructor(snapshot: GraphSnapshot);
|
|
10
|
-
validate(
|
|
11
|
+
validate(options?: {
|
|
12
|
+
readonly onProgress?: (event: GraphValidationProgress) => void;
|
|
13
|
+
}): Promise<{
|
|
11
14
|
conforms: boolean;
|
|
12
15
|
issues: {
|
|
13
16
|
category: "extraction-gap";
|
package/dist/graph/rdf.js
CHANGED
|
@@ -5,6 +5,7 @@ import { contextPackEvidenceAuthority } from "./authority.js";
|
|
|
5
5
|
import { stableJson } from "../infrastructure/util.js";
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { CG, RDF, XSD, evidenceShapes } from './shacl-shapes.js';
|
|
8
|
+
import { measureValidationPhase } from './validation-progress.js';
|
|
8
9
|
const { namedNode: iri, literal } = DataFactory;
|
|
9
10
|
export { CG } from './shacl-shapes.js';
|
|
10
11
|
export const nodeIri = (id) => `urn:context-graph:node:${encodeURIComponent(id)}`;
|
|
@@ -61,16 +62,19 @@ export class RdfEvidenceGraph {
|
|
|
61
62
|
this.dataset.addQuad(iri('urn:context-graph:snapshot'), iri(CG + 'configurationDigest'), literal(snapshot.manifest.configurationDigest));
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
async validate() {
|
|
65
|
+
async validate(options = {}) {
|
|
65
66
|
// These graph-independent shapes already read the dataset as a union. Index
|
|
66
67
|
// that union once so every property lookup avoids scanning one graph per fact.
|
|
67
68
|
// Keep every triple; the original named dataset retains assertion provenance
|
|
68
69
|
// and remains the source for queries and serialization.
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const counts = { factCount: this.snapshot.facts.length };
|
|
71
|
+
const validationGraph = await measureValidationPhase('build-validation-union', counts, options.onProgress, () => {
|
|
72
|
+
const union = new Store();
|
|
73
|
+
for (const { subject, predicate, object } of this.dataset)
|
|
74
|
+
union.addQuad(subject, predicate, object);
|
|
75
|
+
return union;
|
|
76
|
+
});
|
|
77
|
+
const report = await measureValidationPhase('validate-shacl', counts, options.onProgress, () => new SHACLValidator(new Store(evidenceShapes)).validate(validationGraph));
|
|
74
78
|
const issues = report.results.map((result) => ({
|
|
75
79
|
category: 'extraction-gap',
|
|
76
80
|
focus: result.focusNode?.value,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type GraphValidationPhase = 'validate-identities' | 'build-rdf-evidence' | 'build-validation-union' | 'validate-shacl';
|
|
2
|
+
export interface GraphValidationProgress {
|
|
3
|
+
readonly phase: GraphValidationPhase;
|
|
4
|
+
readonly status: 'started' | 'completed' | 'failed';
|
|
5
|
+
/** Wall time for this phase, not a completion estimate or in-phase heartbeat. */
|
|
6
|
+
readonly elapsedMs: number;
|
|
7
|
+
readonly factCount: number;
|
|
8
|
+
readonly nodeCount?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Phase boundaries remain observable even when an operation blocks same-thread timers. */
|
|
11
|
+
export declare function measureValidationPhase<T>(phase: GraphValidationPhase, counts: {
|
|
12
|
+
readonly factCount: number;
|
|
13
|
+
readonly nodeCount?: number;
|
|
14
|
+
}, onProgress: ((event: GraphValidationProgress) => void) | undefined, operation: () => T | Promise<T>): Promise<T>;
|