@weftlet/toolkit 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +15 -0
  2. package/dist/application/application.js +7 -4
  3. package/dist/application/cache-adoption.d.ts +43 -0
  4. package/dist/application/cache-adoption.js +129 -0
  5. package/dist/application/extraction-identity.d.ts +6 -0
  6. package/dist/application/extraction-identity.js +15 -0
  7. package/dist/application/hydration-failure.d.ts +41 -0
  8. package/dist/application/hydration-failure.js +43 -0
  9. package/dist/application/hydration-plan.d.ts +4 -7
  10. package/dist/application/hydration-plan.js +7 -15
  11. package/dist/application/hydrator.d.ts +3 -5
  12. package/dist/application/hydrator.js +23 -48
  13. package/dist/application/overlay.js +10 -3
  14. package/dist/application/saved-evidence.d.ts +6 -3
  15. package/dist/application/saved-evidence.js +7 -1
  16. package/dist/application/semantic-artifact-store.d.ts +4 -0
  17. package/dist/application/semantic-artifact-store.js +7 -0
  18. package/dist/application/semantic-run.d.ts +29 -0
  19. package/dist/application/semantic-run.js +111 -0
  20. package/dist/application/semantic-scope.d.ts +13 -0
  21. package/dist/application/semantic-scope.js +12 -0
  22. package/dist/application/snapshot-checks.d.ts +2 -1
  23. package/dist/application/snapshot-checks.js +8 -1
  24. package/dist/configuration/cache.d.ts +6 -0
  25. package/dist/configuration/cache.js +18 -0
  26. package/dist/extraction/document.js +1 -1
  27. package/dist/extraction/providers.js +10 -3
  28. package/dist/extraction/repository-nodes.d.ts +2 -3
  29. package/dist/extraction/repository-nodes.js +5 -11
  30. package/dist/extraction/routing.d.ts +1 -1
  31. package/dist/extraction/routing.js +3 -6
  32. package/dist/extraction/semantic-cache.d.ts +1 -0
  33. package/dist/extraction/semantic-cache.js +9 -2
  34. package/dist/extraction/semantic-diagnostics.d.ts +9 -0
  35. package/dist/extraction/semantic-diagnostics.js +15 -0
  36. package/dist/extraction/semantic.d.ts +5 -1
  37. package/dist/extraction/semantic.js +39 -6
  38. package/dist/extraction/static.d.ts +13 -5
  39. package/dist/extraction/static.js +8 -15
  40. package/dist/governance/governance.js +11 -7
  41. package/dist/graph/query.js +2 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +3 -0
  44. package/dist/interfaces/cli.js +44 -21
  45. package/dist/storage/storage.d.ts +7 -1
  46. package/dist/storage/storage.js +15 -3
  47. package/dist/visualization/visualization.js +1 -0
  48. package/docs/ADR-016-hydration-preview.md +1 -1
  49. package/docs/ADR-017-shacl-validation-union.md +1 -1
  50. package/docs/ADR-018-operational-semantic-cache.md +100 -0
  51. package/docs/ADR-019-semantic-failure-diagnostics.md +82 -0
  52. package/docs/ADR-020-canonical-physical-file-producers.md +97 -0
  53. package/docs/PROVIDERS.md +51 -1
  54. package/docs/RELEASE-1.0-PLAN.md +70 -21
  55. package/docs/RELEASING.md +4 -2
  56. package/docs/ROADMAP.md +31 -12
  57. package/docs/VALIDATION.md +212 -4
  58. package/docs/extraction-manifest.json +42 -21
  59. package/package.json +2 -2
@@ -5,12 +5,13 @@ import { promisify } from 'node:util';
5
5
  import { scipSourcePatterns } from '../configuration/scip-project.js';
6
6
  import { GitRepository } from "../repository/git.js";
7
7
  import { withCommittedTree } from "../repository/committed-tree.js";
8
- import { RepositoryContractExtractor, RepositoryFileExtractor, ScipSymbolExtractor, ScipTypeScriptIndexer, repositoryFileExtractorIdentity } from "../extraction/static.js";
8
+ import { RepositoryContractExtractor, RepositoryFileExtractor, ScipSymbolExtractor, ScipTypeScriptIndexer, repositoryFileExtractorIdentity, repositoryContractExtractorIdentity, scipSymbolExtractorIdentity } from "../extraction/static.js";
9
9
  import { sha256, stableJson } from "../infrastructure/util.js";
10
10
  import { contextPackEvidenceAuthority } from "../graph/query.js";
11
11
  import { inWorkingScope, readWorkingContent } from "../repository/working-content.js";
12
12
  import { extractRoutingDeclarations, routingExtractorIdentity } from '../extraction/routing.js';
13
13
  import { RdfEvidenceGraph } from '../graph/rdf.js';
14
+ import { graphAssemblyIdentity } from './extraction-identity.js';
14
15
  const execute = promisify(execFile);
15
16
  const implementationExtractors = new Set([
16
17
  'repository-file-extractor', 'repository-contract-extractor', 'scip-symbol-extractor',
@@ -57,7 +58,10 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
57
58
  // identity also binds the extractor so rebuilding an old base cannot replace an old overlay.
58
59
  const snapshotId = `${head}-overlay-${sha256(stableJson({
59
60
  baseContentDigest: base.manifest.contentDigest, overlayDigest,
61
+ assembly: graphAssemblyIdentity,
60
62
  repositoryFileExtractor: repositoryFileExtractorIdentity,
63
+ repositoryContractExtractor: repositoryContractExtractorIdentity,
64
+ scipSymbolExtractor: scipSymbolExtractorIdentity,
61
65
  ...(routingSources.length ? { routingExtractor: routingExtractorIdentity } : {}),
62
66
  })).slice(0, 32)}`;
63
67
  const affected = new Set(captured.map((file) => file.path));
@@ -120,7 +124,10 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
120
124
  manifest: {
121
125
  ...base.manifest, snapshotId, baseSha: head, overlayDigest, overlayCapture,
122
126
  extractorVersions: { ...base.manifest.extractorVersions,
127
+ assembly: graphAssemblyIdentity,
123
128
  [repositoryFileExtractorIdentity.id]: repositoryFileExtractorIdentity.version,
129
+ [repositoryContractExtractorIdentity.id]: repositoryContractExtractorIdentity.version,
130
+ [scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
124
131
  ...(routingSources.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}) },
125
132
  generatedAt: new Date().toISOString(), contentDigest: sha256(stableJson(routingSources.length
126
133
  ? { facts: allFacts, diagnostics: extracted.diagnostics } : allFacts)),
@@ -136,8 +143,8 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
136
143
  ],
137
144
  diagnostics: extracted.diagnostics,
138
145
  };
139
- if (routingSources.length && !(await new RdfEvidenceGraph(overlay).validate()).conforms) {
140
- throw new Error('Draft routing overlay failed SHACL validation; saved evidence was preserved');
146
+ if (!(await new RdfEvidenceGraph(overlay).validate()).conforms) {
147
+ throw new Error('Draft overlay failed SHACL validation; saved evidence was preserved');
141
148
  }
142
149
  if (await repository.resolveRef('HEAD') !== head)
143
150
  throw new Error('HEAD changed during overlay capture; retry');
@@ -5,20 +5,23 @@ export declare class SavedEvidenceQueryService implements ImpactQueryService {
5
5
  private readonly queries;
6
6
  private readonly repository;
7
7
  private readonly configurationDigest;
8
- constructor(queries: ImpactQueryService, repository: GitRepository, configurationDigest: string);
8
+ private readonly expectedExtractors?;
9
+ constructor(queries: ImpactQueryService, repository: GitRepository, configurationDigest: string, expectedExtractors?: Readonly<Record<string, string>> | undefined);
9
10
  scan(request: ImpactScanRequest): Promise<{
10
11
  markdown: string;
11
12
  evidenceState: {
13
+ eligibleForCurrentDiffValidation: false;
14
+ extractionCompatible?: boolean | undefined;
15
+ incompatibleExtractors?: readonly string[] | undefined;
12
16
  stale: boolean;
13
17
  configurationMatches: boolean;
14
- eligibleForCurrentDiffValidation: false;
15
18
  overlayFreshness?: "current" | "stale" | "unknown" | undefined;
16
19
  capturedWorkingChanges?: string[] | undefined;
17
20
  changedSinceCapture?: string[] | undefined;
18
21
  currentCommit: string | undefined;
19
22
  excludedWorkingChanges: string[];
20
23
  };
21
- snapshot: Pick<import("@weftlet/core").SnapshotManifest, "snapshotId" | "commitSha" | "contentDigest" | "configurationDigest" | "scope" | "baseSha" | "overlayDigest" | "overlayCapture">;
24
+ snapshot: Pick<import("@weftlet/core").SnapshotManifest, "snapshotId" | "commitSha" | "contentDigest" | "configurationDigest" | "scope" | "baseSha" | "overlayDigest" | "overlayCapture"> & Partial<Pick<import("@weftlet/core").SnapshotManifest, "extractorVersions">>;
22
25
  capabilities: readonly import("@weftlet/core").GraphNode[];
23
26
  facts: readonly import("@weftlet/core").GraphFact[];
24
27
  invariants: readonly import("@weftlet/core").InvariantResult[];
@@ -1,15 +1,18 @@
1
1
  import { MarkdownContextPackRenderer } from "../graph/query.js";
2
2
  import { inWorkingScope, readWorkingContent } from "../repository/working-content.js";
3
3
  import { sha256 } from "../infrastructure/util.js";
4
+ import { incompatibleSnapshotExtractors } from './extraction-identity.js';
4
5
  /** Read-side metadata; never calls an indexer, model, compiler, or refresh operation. */
5
6
  export class SavedEvidenceQueryService {
6
7
  queries;
7
8
  repository;
8
9
  configurationDigest;
9
- constructor(queries, repository, configurationDigest) {
10
+ expectedExtractors;
11
+ constructor(queries, repository, configurationDigest, expectedExtractors) {
10
12
  this.queries = queries;
11
13
  this.repository = repository;
12
14
  this.configurationDigest = configurationDigest;
15
+ this.expectedExtractors = expectedExtractors;
13
16
  }
14
17
  async scan(request) {
15
18
  const pack = await this.queries.scan(request);
@@ -45,12 +48,15 @@ export class SavedEvidenceQueryService {
45
48
  }
46
49
  }
47
50
  const excludedWorkingChanges = workingChanges.filter((path) => !capturedWorkingChanges.includes(path));
51
+ const incompatibleExtractors = this.expectedExtractors
52
+ ? incompatibleSnapshotExtractors(pack.snapshot.extractorVersions, this.expectedExtractors) : undefined;
48
53
  const evidenceState = {
49
54
  currentCommit,
50
55
  excludedWorkingChanges,
51
56
  ...(overlayFreshness ? { overlayFreshness, capturedWorkingChanges, changedSinceCapture: [...changedSinceCapture].sort() } : {}),
52
57
  stale: currentCommit !== pack.snapshot.commitSha || (overlayFreshness !== undefined && overlayFreshness !== 'current'),
53
58
  configurationMatches: pack.snapshot.configurationDigest === this.configurationDigest,
59
+ ...(incompatibleExtractors ? { extractionCompatible: incompatibleExtractors.length === 0, incompatibleExtractors } : {}),
54
60
  eligibleForCurrentDiffValidation: false,
55
61
  };
56
62
  const result = { ...pack, evidenceState };
@@ -0,0 +1,4 @@
1
+ import type { ContextGraphConfig } from '../configuration/config.js';
2
+ import { SemanticArtifactStore } from '../storage/storage.js';
3
+ /** Shared by planning and execution; construction never creates directories or reads credentials. */
4
+ export declare function createSemanticArtifactStore(repositoryRoot: string, config: ContextGraphConfig, userCacheRoot?: string): SemanticArtifactStore;
@@ -0,0 +1,7 @@
1
+ import { resolveSemanticStorageRoots } from '../configuration/cache.js';
2
+ import { SemanticArtifactStore } from '../storage/storage.js';
3
+ /** Shared by planning and execution; construction never creates directories or reads credentials. */
4
+ export function createSemanticArtifactStore(repositoryRoot, config, userCacheRoot) {
5
+ const { primary, legacy } = resolveSemanticStorageRoots(repositoryRoot, config, userCacheRoot);
6
+ return new SemanticArtifactStore(primary, legacy ? [legacy] : []);
7
+ }
@@ -0,0 +1,29 @@
1
+ import type { ExtractionContext, SemanticSource } from '@weftlet/core';
2
+ import type { ContextGraphConfig } from '../configuration/config.js';
3
+ import type { SemanticHydrator } from '../extraction/semantic.js';
4
+ export interface HydrationProgressEvent {
5
+ readonly phase: string;
6
+ readonly completed?: number;
7
+ readonly total?: number;
8
+ readonly sourceId?: string;
9
+ readonly elapsedMs?: number;
10
+ }
11
+ /** Settle active workers before reporting failure; never publish snapshots here. */
12
+ export declare function hydrateSemanticSources(options: {
13
+ readonly sources: readonly SemanticSource[];
14
+ readonly context: ExtractionContext;
15
+ readonly config: ContextGraphConfig['semantic'];
16
+ readonly configurationDigest: string;
17
+ readonly hydrator: SemanticHydrator;
18
+ readonly forceRevision?: string;
19
+ readonly signal?: AbortSignal;
20
+ readonly onProgress?: (event: HydrationProgressEvent) => void;
21
+ }): Promise<{
22
+ hydrated: readonly {
23
+ artifact: import("@weftlet/core").SemanticArtifact;
24
+ reused: boolean;
25
+ source: SemanticSource;
26
+ }[];
27
+ semanticExtracted: number;
28
+ semanticReused: number;
29
+ }>;
@@ -0,0 +1,111 @@
1
+ import { semanticSourceId } from '../extraction/semantic-diagnostics.js';
2
+ import { SemanticHydrationError, semanticFailureCategory, semanticFailureCode } from './hydration-failure.js';
3
+ async function concurrentMap(values, concurrency, worker, shouldStop) {
4
+ const results = new Array(values.length);
5
+ let cursor = 0;
6
+ let failure;
7
+ let failed = false;
8
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, values.length)) }, async () => {
9
+ while (!failed && !shouldStop() && cursor < values.length) {
10
+ const index = cursor++;
11
+ try {
12
+ results[index] = await worker(values[index], index);
13
+ }
14
+ catch (error) {
15
+ failed = true;
16
+ failure ??= error;
17
+ }
18
+ }
19
+ }));
20
+ if (failed)
21
+ throw failure;
22
+ return results;
23
+ }
24
+ /** Settle active workers before reporting failure; never publish snapshots here. */
25
+ export async function hydrateSemanticSources(options) {
26
+ const { sources, context, config, configurationDigest, hydrator, forceRevision } = options;
27
+ const { commitSha } = context;
28
+ const runStarted = performance.now();
29
+ let semanticExtracted = 0;
30
+ let semanticReused = 0;
31
+ let started = 0;
32
+ let failed = 0;
33
+ let cancelled = 0;
34
+ let firstFailure;
35
+ const controller = new AbortController();
36
+ const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
37
+ const hydrated = await concurrentMap(sources, config.concurrency, async (source) => {
38
+ started += 1;
39
+ const sourceId = semanticSourceId(context.repositoryId, source);
40
+ const sourceStarted = performance.now();
41
+ let phase = 'semantic-cache-read';
42
+ options.onProgress?.({ phase: 'semantic-source-started', sourceId,
43
+ completed: semanticExtracted + semanticReused, total: sources.length });
44
+ let result;
45
+ try {
46
+ result = await hydrator.hydrate(source, context, forceRevision, { signal, onPhase: value => { phase = value; } });
47
+ }
48
+ catch (error) {
49
+ // Record the initiating failure before abort synchronously notifies other workers.
50
+ const code = semanticFailureCode(error, phase, signal);
51
+ if (code === 'cancelled')
52
+ cancelled += 1;
53
+ else
54
+ failed += 1;
55
+ firstFailure ??= { code, sourceId, phase, elapsedScope: 'source', elapsedMs: Math.round(performance.now() - sourceStarted) };
56
+ controller.abort();
57
+ throw error;
58
+ }
59
+ if (result.reused)
60
+ semanticReused += 1;
61
+ else
62
+ semanticExtracted += 1;
63
+ options.onProgress?.({ phase: result.reused ? 'semantic-cache-hit' : 'semantic-extracted',
64
+ sourceId, elapsedMs: Math.round(performance.now() - sourceStarted),
65
+ completed: semanticReused + semanticExtracted, total: sources.length });
66
+ return { source, ...result };
67
+ }, () => signal.aborted).catch(() => {
68
+ if (!firstFailure)
69
+ throw new Error('Semantic hydration failed before source processing could be reported');
70
+ return undefined;
71
+ });
72
+ // Cancellation can arrive during a durable write or completion callback, without
73
+ // a failed source. Do not relabel successfully retained work as cancelled.
74
+ if (!firstFailure && signal.aborted)
75
+ firstFailure = { code: 'cancelled', phase: 'semantic-scheduling',
76
+ elapsedScope: 'semantic-run', elapsedMs: Math.round(performance.now() - runStarted) };
77
+ if (firstFailure) {
78
+ // concurrentMap settles every active worker before returning: these are final counts.
79
+ const completed = semanticExtracted + semanticReused;
80
+ const recovery = [
81
+ 'No snapshot was published by this run. Previously published evidence and completed artifacts were retained.',
82
+ 'Inspect hydrate --plan --json locally with the same --config, committed --ref and scope options to map sourceId to its path and check expected reuse. The preview contains local paths; review it before sharing.',
83
+ ];
84
+ if (firstFailure.code === 'timeout')
85
+ recovery.push('Review semantic.timeoutMs before retrying. This limit is per Codex attempt (authentication is separately bounded), or covers module import/factory/extraction; source elapsed time can be longer.');
86
+ else if (firstFailure.code === 'unavailable')
87
+ recovery.push('Run doctor with the same configuration to check provider installation and authentication.');
88
+ else if (firstFailure.code === 'cache-failure')
89
+ recovery.push('Inspect cache integrity and permissions. Corrupt or conflicting complete artifacts are retained and must be repaired explicitly; a timeout change cannot repair them.');
90
+ else if (firstFailure.code === 'invalid-output')
91
+ recovery.push('Review source coverage and extraction policy locally; invalid output is an extraction gap, not an approved rule or a passing validation.');
92
+ else if (firstFailure.code === 'unresolved' || firstFailure.code === 'low-confidence')
93
+ recovery.push('Review cited sources and configured confidence/unresolved policy locally. Required evidence needs review; a policy rejection is not approved intent or a passing validation.');
94
+ else if (firstFailure.code === 'exit' || firstFailure.code === 'provider-failure')
95
+ recovery.push('Check provider readiness locally. Private provider messages are omitted; this failure is not proof of a timeout.');
96
+ recovery.push(forceRevision
97
+ ? 'This run used --force-semantic. Its completed revision artifacts remain stored, but an ordinary rerun or a new forced run does not automatically reuse that revision.'
98
+ : 'Rerun hydrate without --force-semantic after resolving the failure; only valid compatible completed artifacts are reusable.');
99
+ recovery.push(config.provider === 'codex-cli'
100
+ ? 'Built-in Codex supports reuse across operational timeout changes. For profiles created before shared caching, use cache adopt --from-config with the exact previous configuration; preview with --dry-run first.'
101
+ : 'Custom provider identities include their full semantic configuration; changing a module timeout may invalidate reuse. Preview again before extraction.');
102
+ throw new SemanticHydrationError({ schemaVersion: 1, kind: 'semantic-hydration-failure', ...firstFailure,
103
+ category: semanticFailureCategory(firstFailure.code), commitSha, configurationDigest,
104
+ timeoutMs: config.timeoutMs ?? 120_000,
105
+ timeoutScope: config.provider === 'codex-cli' ? 'provider-attempt' : 'module-execution',
106
+ progress: { total: sources.length, completed, extracted: semanticExtracted, reused: semanticReused,
107
+ failed, cancelled, notStarted: sources.length - started, remaining: sources.length - completed },
108
+ forced: !!forceRevision, recovery });
109
+ }
110
+ return { hydrated: hydrated, semanticExtracted, semanticReused };
111
+ }
@@ -0,0 +1,13 @@
1
+ import type { ImpactScanRequest } from '@weftlet/core';
2
+ import type { ContextGraphConfig } from '../configuration/config.js';
3
+ import type { GitRepository } from '../repository/git.js';
4
+ export interface SemanticScope {
5
+ readonly mode: 'configured' | 'agent';
6
+ readonly selectors: readonly string[];
7
+ readonly digest: string;
8
+ readonly headName: string;
9
+ readonly omittedDocuments: readonly string[];
10
+ readonly ignoredSearchTerms: number;
11
+ }
12
+ /** Resolve the same committed selection for previews and explicit artifact recovery. */
13
+ export declare function resolveSemanticScope(config: ContextGraphConfig, repository: GitRepository, commitSha: string, request?: ImpactScanRequest): Promise<SemanticScope>;
@@ -0,0 +1,12 @@
1
+ import { sha256, stableJson } from '../infrastructure/util.js';
2
+ import { AgentSemanticScopeResolver } from './agent-context.js';
3
+ /** Resolve the same committed selection for previews and explicit artifact recovery. */
4
+ export async function resolveSemanticScope(config, repository, commitSha, request = {}) {
5
+ const agent = config.agentContext
6
+ ? await new AgentSemanticScopeResolver(config, repository).resolve(request, commitSha)
7
+ : undefined;
8
+ const selectors = agent?.semanticSources ?? config.semanticSources;
9
+ return { mode: agent ? 'agent' : 'configured', selectors, digest: sha256(stableJson(selectors)),
10
+ headName: agent?.headName ?? 'default', omittedDocuments: agent?.omittedDocuments ?? [],
11
+ ignoredSearchTerms: agent?.ignoredSearchTerms ?? 0 };
12
+ }
@@ -8,7 +8,8 @@ export declare class SnapshotChecks {
8
8
  private readonly hydrator;
9
9
  private readonly scopeResolver?;
10
10
  private readonly configurationDigest?;
11
- constructor(store: Pick<SnapshotStore, 'readHead'>, repository: Pick<GitRepository, 'resolveRef'> & Partial<Pick<GitRepository, 'workingChanges'>>, hydrator: Pick<ContextGraphHydrator, 'compareClean'>, scopeResolver?: Pick<AgentSemanticScopeResolver, "resolve"> | undefined, configurationDigest?: string | undefined);
11
+ private readonly expectedExtractors;
12
+ constructor(store: Pick<SnapshotStore, 'readHead'>, repository: Pick<GitRepository, 'resolveRef'> & Partial<Pick<GitRepository, 'workingChanges'>>, hydrator: Pick<ContextGraphHydrator, 'compareClean'>, scopeResolver?: Pick<AgentSemanticScopeResolver, "resolve"> | undefined, configurationDigest?: string | undefined, expectedExtractors?: Readonly<Record<string, string>>);
12
13
  private target;
13
14
  private readTarget;
14
15
  read(request: ImpactScanRequest, ref?: string): Promise<import("@weftlet/core").GraphSnapshot>;
@@ -1,3 +1,4 @@
1
+ import { incompatibleSnapshotExtractors, requiredSnapshotExtractorVersions } from './extraction-identity.js';
1
2
  // Final checks must use the same scope resolver as prepared impact scans, without silently
2
3
  // starting hydration or substituting another owner's (or an older commit's) evidence.
3
4
  export class SnapshotChecks {
@@ -6,12 +7,14 @@ export class SnapshotChecks {
6
7
  hydrator;
7
8
  scopeResolver;
8
9
  configurationDigest;
9
- constructor(store, repository, hydrator, scopeResolver, configurationDigest) {
10
+ expectedExtractors;
11
+ constructor(store, repository, hydrator, scopeResolver, configurationDigest, expectedExtractors = requiredSnapshotExtractorVersions()) {
10
12
  this.store = store;
11
13
  this.repository = repository;
12
14
  this.hydrator = hydrator;
13
15
  this.scopeResolver = scopeResolver;
14
16
  this.configurationDigest = configurationDigest;
17
+ this.expectedExtractors = expectedExtractors;
15
18
  }
16
19
  async target(request, ref) {
17
20
  if (request.snapshotId)
@@ -38,6 +41,10 @@ export class SnapshotChecks {
38
41
  if (this.configurationDigest && snapshot.manifest.configurationDigest !== this.configurationDigest) {
39
42
  throw new Error('Snapshot configuration differs from current configuration; refresh required');
40
43
  }
44
+ const incompatible = incompatibleSnapshotExtractors(snapshot.manifest.extractorVersions, this.expectedExtractors);
45
+ if (incompatible.length) {
46
+ throw new Error(`Snapshot extraction versions require refresh (${incompatible.join(', ')}). Historical evidence remains queryable; hydrate with the current toolkit before final validation.`);
47
+ }
41
48
  return snapshot;
42
49
  }
43
50
  async read(request, ref = 'HEAD') {
@@ -1,2 +1,8 @@
1
1
  import type { ContextGraphConfig } from "./config.js";
2
2
  export declare function resolveStorageRoot(repositoryRoot: string, config: ContextGraphConfig, userCacheRoot?: string): string;
3
+ export declare function semanticCachePartition(repositoryRoot: string, config: ContextGraphConfig): string;
4
+ /** Snapshot profiles retain the full configuration. Only Codex user artifacts share operational changes. */
5
+ export declare function resolveSemanticStorageRoots(repositoryRoot: string, config: ContextGraphConfig, userCacheRoot?: string): {
6
+ primary: string;
7
+ legacy?: string;
8
+ };
@@ -8,3 +8,21 @@ export function resolveStorageRoot(repositoryRoot, config, userCacheRoot = resol
8
8
  const profile = sha256(stableJson({ config, checkout: resolve(repositoryRoot) })).slice(0, 20);
9
9
  return resolve(userCacheRoot, profile);
10
10
  }
11
+ // These controls do not change a successful built-in Codex extraction. Unknown
12
+ // fields and module-provider configuration remain conservative identity inputs.
13
+ const codexOperationalFields = new Set([
14
+ 'timeoutMs', 'retries', 'concurrency', 'maxDocuments', 'maxSourceBytes',
15
+ ]);
16
+ export function semanticCachePartition(repositoryRoot, config) {
17
+ const semantic = config.semantic.provider === 'codex-cli'
18
+ ? Object.fromEntries(Object.entries(config.semantic).filter(([field]) => !codexOperationalFields.has(field)))
19
+ : config.semantic;
20
+ return sha256(stableJson({ config: { ...config, semantic }, checkout: resolve(repositoryRoot) }));
21
+ }
22
+ /** Snapshot profiles retain the full configuration. Only Codex user artifacts share operational changes. */
23
+ export function resolveSemanticStorageRoots(repositoryRoot, config, userCacheRoot = resolve(homedir(), '.cache', 'context-graph')) {
24
+ const legacy = resolveStorageRoot(repositoryRoot, config, userCacheRoot);
25
+ if (config.storageScope !== 'user' || config.semantic.provider !== 'codex-cli')
26
+ return { primary: legacy };
27
+ return { primary: resolve(userCacheRoot, 'semantic-artifacts-v1', semanticCachePartition(repositoryRoot, config)), legacy };
28
+ }
@@ -1,6 +1,6 @@
1
1
  import { sha256, stableJson } from '../infrastructure/util.js';
2
2
  import { documentReferences, resolveDocumentReference } from './document-references.js';
3
- import { repositoryFileNode as fileNode } from './repository-nodes.js';
3
+ import { repositoryFileNode as fileNode } from '@weftlet/core';
4
4
  export const documentLinkerIdentity = { id: 'document-linker', version: '2.0.0' };
5
5
  export function linkSemanticDocuments(context, hydrated, trackedPaths, approvedSourcePaths = []) {
6
6
  const facts = new Map();
@@ -31,14 +31,21 @@ export function createSemanticExtractor(config, repositoryRoot) {
31
31
  Promise.resolve().then(async () => {
32
32
  checkpoint();
33
33
  if (!extractor) {
34
- const module = await import(resolveConsumerModule(modulePath, repositoryRoot));
34
+ let module;
35
+ try {
36
+ module = await import(resolveConsumerModule(modulePath, repositoryRoot));
37
+ }
38
+ catch {
39
+ checkpoint();
40
+ throw new SemanticProviderError('unavailable', 'Semantic provider module could not be loaded');
41
+ }
35
42
  checkpoint();
36
43
  if (typeof module.createSemanticExtractor !== 'function')
37
- throw new Error('Provider module must export createSemanticExtractor(config)');
44
+ throw new SemanticProviderError('unavailable', 'Provider module must export createSemanticExtractor(config)');
38
45
  const candidate = await module.createSemanticExtractor(config, { ...options, signal });
39
46
  checkpoint();
40
47
  if (candidate?.identity !== config.providerIdentity || typeof candidate.extract !== 'function') {
41
- throw new Error('Provider identity/contract does not match configuration');
48
+ throw new SemanticProviderError('unavailable', 'Provider identity/contract does not match configuration');
42
49
  }
43
50
  extractor = candidate;
44
51
  }
@@ -1,7 +1,6 @@
1
- import type { GraphNode } from '@weftlet/core';
1
+ import { type GraphNode } from '@weftlet/core';
2
+ export { repositoryFileNode } from '@weftlet/core';
2
3
  /** Internal cache contract: changing this policy must also change snapshot assembly identity. */
3
4
  export declare const repositoryNodeIdentityVersion = 1;
4
- /** Physical file identity; this does not establish existence, intent, ownership or approval. */
5
- export declare function repositoryFileNode(path: string): GraphNode;
6
5
  /** Normalize only reserved document/file identities, never inferred business concept IDs. */
7
6
  export declare function canonicalizeRepositoryDocumentNode(node: GraphNode): GraphNode;
@@ -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 || !isDocumentPath(match[1]))
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.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))
@@ -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
- value = await store.read(key);
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, reason: 'Readable artifact matches the source and extraction contract; reuse remains subject to unchanged inputs.', artifact: value };
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): 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
  }