@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.
Files changed (72) hide show
  1. package/README.md +80 -0
  2. package/dist/application/agent-context.d.ts +3 -1
  3. package/dist/application/agent-context.js +15 -6
  4. package/dist/application/application.js +7 -4
  5. package/dist/application/cache-adoption.d.ts +43 -0
  6. package/dist/application/cache-adoption.js +129 -0
  7. package/dist/application/extraction-identity.d.ts +6 -0
  8. package/dist/application/extraction-identity.js +15 -0
  9. package/dist/application/hydration-failure.d.ts +41 -0
  10. package/dist/application/hydration-failure.js +43 -0
  11. package/dist/application/hydration-plan.d.ts +54 -0
  12. package/dist/application/hydration-plan.js +71 -0
  13. package/dist/application/hydrator.d.ts +3 -5
  14. package/dist/application/hydrator.js +30 -61
  15. package/dist/application/overlay.js +10 -3
  16. package/dist/application/saved-evidence.d.ts +6 -3
  17. package/dist/application/saved-evidence.js +7 -1
  18. package/dist/application/semantic-artifact-store.d.ts +4 -0
  19. package/dist/application/semantic-artifact-store.js +7 -0
  20. package/dist/application/semantic-run.d.ts +29 -0
  21. package/dist/application/semantic-run.js +111 -0
  22. package/dist/application/semantic-scope.d.ts +13 -0
  23. package/dist/application/semantic-scope.js +12 -0
  24. package/dist/application/snapshot-checks.d.ts +2 -1
  25. package/dist/application/snapshot-checks.js +8 -1
  26. package/dist/configuration/cache.d.ts +6 -0
  27. package/dist/configuration/cache.js +18 -0
  28. package/dist/extraction/document.js +1 -1
  29. package/dist/extraction/providers.js +12 -5
  30. package/dist/extraction/repository-nodes.d.ts +2 -3
  31. package/dist/extraction/repository-nodes.js +5 -11
  32. package/dist/extraction/routing.d.ts +1 -1
  33. package/dist/extraction/routing.js +3 -6
  34. package/dist/extraction/semantic-cache.d.ts +13 -0
  35. package/dist/extraction/semantic-cache.js +66 -0
  36. package/dist/extraction/semantic-diagnostics.d.ts +9 -0
  37. package/dist/extraction/semantic-diagnostics.js +15 -0
  38. package/dist/extraction/semantic-identity.d.ts +7 -0
  39. package/dist/extraction/semantic-identity.js +29 -0
  40. package/dist/extraction/semantic-sources.d.ts +27 -0
  41. package/dist/extraction/semantic-sources.js +35 -0
  42. package/dist/extraction/semantic.d.ts +5 -1
  43. package/dist/extraction/semantic.js +47 -24
  44. package/dist/extraction/static.d.ts +13 -5
  45. package/dist/extraction/static.js +8 -15
  46. package/dist/governance/governance.js +11 -7
  47. package/dist/graph/query.js +2 -0
  48. package/dist/graph/rdf.d.ts +1 -1
  49. package/dist/graph/rdf.js +13 -28
  50. package/dist/graph/shacl-shapes.d.ts +5 -0
  51. package/dist/graph/shacl-shapes.js +27 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +4 -0
  54. package/dist/interfaces/cli.js +86 -19
  55. package/dist/repository/git.d.ts +1 -1
  56. package/dist/repository/git.js +5 -1
  57. package/dist/storage/storage.d.ts +7 -1
  58. package/dist/storage/storage.js +15 -3
  59. package/dist/visualization/visualization.js +1 -0
  60. package/docs/ADR-016-hydration-preview.md +91 -0
  61. package/docs/ADR-017-shacl-validation-union.md +102 -0
  62. package/docs/ADR-018-operational-semantic-cache.md +100 -0
  63. package/docs/ADR-019-semantic-failure-diagnostics.md +82 -0
  64. package/docs/ADR-020-canonical-physical-file-producers.md +97 -0
  65. package/docs/PROVIDERS.md +51 -1
  66. package/docs/RELEASE-1.0-PLAN.md +148 -32
  67. package/docs/RELEASING.md +19 -6
  68. package/docs/ROADMAP.md +46 -5
  69. package/docs/VALIDATION.md +341 -3
  70. package/docs/WEFTLET-2.0-PLAN.md +9 -8
  71. package/docs/extraction-manifest.json +55 -23
  72. 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.0.0";
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.0.0' };
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.`, [], [`document:${path}`]);
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): Promise<{
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, repositoryNodeIdentityVersion } from './repository-nodes.js';
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 = `codex-cli:${config.model}:${config.reasoningEffort}:${config.promptVersion}:contract-v3`;
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
- options.signal?.throwIfAborted();
336
- const keyInput = {
337
- stableSemanticId: source.stableSemanticId,
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
- const key = sha256(stableJson(keyInput));
351
- const existing = await this.store.read(key);
352
- if (existing)
353
- return { artifact: existing, reused: true };
354
- const extraction = await this.extractor.extract(source, context, options);
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 Error(`Low-confidence semantic assertion for ${source.path}`);
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 Error(`Unresolved semantic mappings for ${source.path}: ${[...unresolvedMappings, ...diagnostics.map((item) => item.reason)].join('; ')}`);
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 = sha256(stableJson({ facts, diagnostics, compiledOml, rawModelOutput: extraction.rawModelOutput }));
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 { ExtractionContext, FactExtractor, GraphFact } from "@weftlet/core";
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: string;
25
- version: string;
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: string;
52
- version: string;
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 = { id: 'scip-symbol-extractor', version: '1.0.0' };
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
- id: `component:file:${path}`,
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 = { id: 'repository-contract-extractor', version: '1.1.0' };
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: { id: `component:file:${module.source}`, kind: 'Component', label: module.source,
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: { id: `component:file:${dependency.resolved}`, kind: 'Component', label: dependency.resolved },
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', { id: `component:file:${path}`, kind: 'Component', label: path });
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: '2' } }));
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: { 'dependency-cruiser': '18.7.0' },
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(`component:file:${m.source}`) === row.from)?.source;
162
- const to = modules.find((m) => nodeIri(`component:file:${m.source}`) === row.to)?.source;
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
  }
@@ -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,
@@ -1,5 +1,5 @@
1
1
  import type { GraphFact, GraphSnapshot } from "@weftlet/core";
2
- export declare const CG = "https://context-graph.dev/record#";
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. */