@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
package/README.md CHANGED
@@ -101,6 +101,12 @@ evidence roots; selected validation documents still undergo semantic extraction
101
101
  SOT traversal and default validation-document separation remain T07 work. See
102
102
  [ADR 016](docs/ADR-016-hydration-preview.md) and the [1.0 task list](docs/RELEASE-1.0-PLAN.md).
103
103
 
104
+ Starting with **0.4.0**, preview also maps opaque diagnostic source IDs to local paths. Semantic
105
+ failures report completed/remaining work and recovery guidance; `hydrate --json --quiet` emits
106
+ their structured report on stderr. Built-in Codex can reuse completed artifacts after supported
107
+ operational changes such as increasing a timeout. See [provider recovery](docs/PROVIDERS.md#limits-and-reuse)
108
+ for preview, retry and explicit legacy-cache adoption instructions.
109
+
104
110
  ## SHACL validation and performance
105
111
 
106
112
  Starting with 0.3.0, the toolkit indexes a temporary union to accelerate cold validation.
@@ -172,6 +178,15 @@ For tests excluded by the build config or workspace packages whose outputs are a
172
178
  committed archives, configure an [explicit TypeScript source project](docs/ADR-010-committed-source-indexing.md)
173
179
  with selected files and local package aliases.
174
180
 
181
+ Repository adapters using **0.4.0 or newer** should import `repositoryFileNode` from
182
+ `@weftlet/core` for physical-file nodes. It preserves canonical document IDs, spec/test Evidence
183
+ and production Component kinds, including test files containing mocked calls or decorators.
184
+ Keep business-concept nodes separate; see the [core API example](packages/core/README.md) and
185
+ [producer decision](docs/ADR-020-canonical-physical-file-producers.md). After upgrading, rebuild
186
+ the graph with compatible cached artifacts; historical snapshots remain queryable but cannot
187
+ satisfy current validation with old extraction versions. Consumer-owned adapters must be migrated
188
+ as well; the toolkit cannot correct nodes they still classify independently.
189
+
175
190
  In a selected document such as `docs/intent.md`, link a responsibility to its sources:
176
191
 
177
192
  ```md
@@ -10,11 +10,13 @@ import { OpenCaesarOntologyCompiler, DirectRdfOntologyCompiler } from "../extrac
10
10
  import { SnapshotImpactQueryService } from "../graph/query.js";
11
11
  import { SemanticHydrator } from "../extraction/semantic.js";
12
12
  import { createSemanticExtractor } from "../extraction/providers.js";
13
- import { SemanticArtifactStore, FileSnapshotStore } from "../storage/storage.js";
13
+ import { FileSnapshotStore } from "../storage/storage.js";
14
+ import { createSemanticArtifactStore } from './semantic-artifact-store.js';
14
15
  import { RepositoryContractExtractor, RepositoryFileExtractor, ScipSymbolExtractor, ScipTypeScriptIndexer, } from "../extraction/static.js";
15
16
  import { OtlpJsonRuntimeEvidenceProvider } from "../extraction/telemetry.js";
16
17
  import { SnapshotChecks } from "./snapshot-checks.js";
17
18
  import { resolveConsumerModule } from "../infrastructure/module-resolution.js";
19
+ import { requiredSnapshotExtractorVersions } from './extraction-identity.js';
18
20
  const emptyAdapter = {
19
21
  factExtractors: [],
20
22
  invariantEvaluators: [],
@@ -45,7 +47,7 @@ export async function createContextGraphApplication(configPath) {
45
47
  const repository = new GitRepository(repositoryRoot);
46
48
  const adapter = await loadContextGraphAdapterModule(config.adapter, repository, repositoryRoot);
47
49
  const snapshotStore = new FileSnapshotStore(storageRoot);
48
- const artifactStore = new SemanticArtifactStore(storageRoot);
50
+ const artifactStore = createSemanticArtifactStore(repositoryRoot, config);
49
51
  const semanticExtractor = createSemanticExtractor(config.semantic, repositoryRoot);
50
52
  const semanticHydrator = new SemanticHydrator(config.semantic, artifactStore, semanticExtractor);
51
53
  const staticIndexer = new ScipTypeScriptIndexer(config.scip, storageRoot);
@@ -75,7 +77,8 @@ export async function createContextGraphApplication(configPath) {
75
77
  const rawQueryService = scopeResolver
76
78
  ? new PreparedImpactQueryService(snapshotStore, hydrator, scopeResolver)
77
79
  : new SnapshotImpactQueryService(snapshotStore, headName);
78
- const queryService = new SavedEvidenceQueryService(rawQueryService, repository, sha256(stableJson(config)));
80
+ const expectedExtractors = requiredSnapshotExtractorVersions(config);
81
+ const queryService = new SavedEvidenceQueryService(rawQueryService, repository, sha256(stableJson(config)), expectedExtractors);
79
82
  return {
80
83
  ...loaded,
81
84
  storageRoot,
@@ -84,7 +87,7 @@ export async function createContextGraphApplication(configPath) {
84
87
  ontologyCompiler,
85
88
  hydrator,
86
89
  queryService,
87
- snapshotChecks: new SnapshotChecks(snapshotStore, repository, hydrator, scopeResolver, sha256(stableJson(config))),
90
+ snapshotChecks: new SnapshotChecks(snapshotStore, repository, hydrator, scopeResolver, sha256(stableJson(config)), expectedExtractors),
88
91
  scopeResolver,
89
92
  headName,
90
93
  };
@@ -0,0 +1,43 @@
1
+ import type { ImpactScanRequest } from '@weftlet/core';
2
+ import { type SemanticInputIssue, type SemanticSelection } from '../extraction/semantic-sources.js';
3
+ import { type SemanticScope } from './semantic-scope.js';
4
+ export interface SemanticCacheAdoptionOptions {
5
+ readonly fromConfigPath: string;
6
+ readonly ref?: string;
7
+ readonly dryRun?: boolean;
8
+ readonly request?: ImpactScanRequest;
9
+ }
10
+ export type SemanticCacheAdoptionStatus = 'adopted' | 'reusable' | 'planned' | 'missing' | 'corrupt' | 'unavailable' | 'conflict';
11
+ export interface SemanticCacheAdoptionRecord {
12
+ readonly path: string;
13
+ readonly key: string;
14
+ readonly status: SemanticCacheAdoptionStatus;
15
+ readonly reason: string;
16
+ }
17
+ export interface SemanticCacheAdoption {
18
+ readonly schemaVersion: 1;
19
+ readonly kind: 'semantic-cache-adoption';
20
+ readonly repositoryId: string;
21
+ readonly ref: string;
22
+ readonly commitSha: string;
23
+ readonly configurationDigest: string;
24
+ readonly previousConfigurationDigest: string;
25
+ readonly dryRun: boolean;
26
+ readonly ready: boolean;
27
+ readonly scope: SemanticScope & {
28
+ readonly complete: boolean;
29
+ };
30
+ readonly records: readonly SemanticCacheAdoptionRecord[];
31
+ readonly counts: Readonly<Record<SemanticCacheAdoptionStatus, number>>;
32
+ readonly selections: readonly SemanticSelection[];
33
+ readonly budgets: {
34
+ readonly maxDocuments: number;
35
+ readonly maxSourceBytes: number;
36
+ readonly selectedDocuments: number;
37
+ readonly totalSourceBytes: number;
38
+ };
39
+ readonly issues: readonly SemanticInputIssue[];
40
+ readonly assumptions: readonly string[];
41
+ }
42
+ /** Explicitly adopts only committed-source artifacts from a compatible, named legacy profile. */
43
+ export declare function adoptSemanticCache(configPath: string, options: SemanticCacheAdoptionOptions): Promise<SemanticCacheAdoption>;
@@ -0,0 +1,129 @@
1
+ import { loadConfig } from '../configuration/config.js';
2
+ import { resolveSemanticStorageRoots, resolveStorageRoot, semanticCachePartition } from '../configuration/cache.js';
3
+ import { inspectSemanticCache } from '../extraction/semantic-cache.js';
4
+ import { semanticProviderIdentity } from '../extraction/semantic-identity.js';
5
+ import { selectSemanticInputs } from '../extraction/semantic-sources.js';
6
+ import { sha256, stableJson } from '../infrastructure/util.js';
7
+ import { GitRepository } from '../repository/git.js';
8
+ import { SemanticArtifactStore } from '../storage/storage.js';
9
+ import { resolveSemanticScope } from './semantic-scope.js';
10
+ const problemStatuses = new Set(['corrupt', 'unavailable', 'conflict']);
11
+ function problem(inspection, source, location) {
12
+ if (inspection.status !== 'corrupt' && inspection.status !== 'unavailable')
13
+ return undefined;
14
+ return { path: source.path, key: inspection.key, status: inspection.status,
15
+ reason: `${location} cache: ${inspection.reason}` };
16
+ }
17
+ async function inspectCandidate(source, config, identity, previous, current) {
18
+ const [before, existing] = await Promise.all([
19
+ inspectSemanticCache(previous, config, source, identity),
20
+ inspectSemanticCache(current, config, source, identity),
21
+ ]);
22
+ const failure = problem(existing, source, 'Current') ?? problem(before, source, 'Previous');
23
+ if (failure)
24
+ return { source, record: failure };
25
+ const base = { path: source.path, key: existing.key };
26
+ if (existing.status === 'hit') {
27
+ const conflict = before.status === 'hit' && before.artifact.outputDigest !== existing.artifact.outputDigest;
28
+ return { source, record: { ...base, status: conflict ? 'conflict' : 'reusable', reason: conflict
29
+ ? 'Current and previous caches contain different valid artifacts for this key; neither was overwritten.'
30
+ : 'A validated artifact is already available in the current cache.' } };
31
+ }
32
+ if (before.status === 'miss')
33
+ return { source, record: { ...base, status: 'missing',
34
+ reason: 'The explicit previous profile has no complete artifact for this source and extraction contract.' } };
35
+ if (before.status !== 'hit' || existing.status !== 'miss')
36
+ throw new Error('Unexpected cache inspection state');
37
+ return { source, artifact: before.artifact, record: { ...base, status: 'planned',
38
+ reason: 'A validated previous artifact can be copied without extraction or changing its provenance.' } };
39
+ }
40
+ async function publishCandidate(candidate, config, identity, current) {
41
+ let publicationError;
42
+ let published = false;
43
+ try {
44
+ await current.write(candidate.artifact);
45
+ published = true;
46
+ }
47
+ catch (error) {
48
+ publicationError = error;
49
+ }
50
+ // Reinspect even after a successful write: an immutable-key winner can have
51
+ // matching output but corrupt identity metadata. A digest match alone is not
52
+ // permission to report that artifact as reusable.
53
+ const existing = await inspectSemanticCache(current, config, candidate.source, identity);
54
+ const failure = problem(existing, candidate.source, 'Current');
55
+ if (failure)
56
+ return failure;
57
+ if (existing.status === 'hit' && existing.artifact.outputDigest !== candidate.artifact.outputDigest) {
58
+ return { ...candidate.record, status: 'conflict',
59
+ reason: 'A concurrent writer published a different valid artifact; neither artifact was overwritten.' };
60
+ }
61
+ if (!published) {
62
+ throw new Error('Unable to adopt a semantic artifact because storage publication failed.', { cause: publicationError });
63
+ }
64
+ if (existing.status !== 'hit')
65
+ return { ...candidate.record, status: 'unavailable',
66
+ reason: 'Published artifact is no longer readable; no successful adoption is claimed.' };
67
+ return { ...candidate.record, status: 'adopted',
68
+ reason: 'Validated artifact is available in the current cache; the previous artifact is unchanged.' };
69
+ }
70
+ /** Explicitly adopts only committed-source artifacts from a compatible, named legacy profile. */
71
+ export async function adoptSemanticCache(configPath, options) {
72
+ const [loaded, previous] = await Promise.all([loadConfig(configPath), loadConfig(options.fromConfigPath)]);
73
+ const { config, repositoryRoot } = loaded;
74
+ if (repositoryRoot !== previous.repositoryRoot) {
75
+ throw new Error('Cache adoption requires both configurations to resolve to the same checkout.');
76
+ }
77
+ if (semanticCachePartition(repositoryRoot, config) !== semanticCachePartition(repositoryRoot, previous.config)) {
78
+ throw new Error('Cache adoption requires matching non-operational configuration and provider contracts.');
79
+ }
80
+ const repository = new GitRepository(repositoryRoot);
81
+ const ref = options.ref ?? 'HEAD';
82
+ const commitSha = await repository.resolveRef(ref);
83
+ const scope = await resolveSemanticScope(config, repository, commitSha, options.request);
84
+ const selection = await selectSemanticInputs(repository, commitSha, scope.selectors, config.semantic);
85
+ const issues = [...selection.issues];
86
+ const limited = scope.omittedDocuments.length > 0 || scope.ignoredSearchTerms > 0;
87
+ const complete = !limited && selection.selections.every((item) => item.status === 'matched');
88
+ if (limited)
89
+ issues.push({ code: 'selection-limited', severity: 'warning',
90
+ message: `Existing agent selection limits omitted ${scope.omittedDocuments.length} documents and ${scope.ignoredSearchTerms} search terms; adoption describes only the selected scope.`,
91
+ paths: scope.omittedDocuments });
92
+ const dryRun = options.dryRun ?? false;
93
+ const records = [];
94
+ if (!issues.some((issue) => issue.severity === 'error')) {
95
+ const current = new SemanticArtifactStore(resolveSemanticStorageRoots(repositoryRoot, config).primary);
96
+ // Do not scan sibling profiles or infer ownership from an artifact key. The
97
+ // supplied old configuration proves the exact legacy location to inspect.
98
+ const old = new SemanticArtifactStore(resolveStorageRoot(repositoryRoot, previous.config));
99
+ const identity = semanticProviderIdentity(config.semantic);
100
+ for (const source of selection.sources) {
101
+ const candidate = await inspectCandidate(source, config.semantic, identity, old, current);
102
+ const result = candidate.artifact && !dryRun
103
+ ? await publishCandidate(candidate, config.semantic, identity, current) : candidate.record;
104
+ records.push(result);
105
+ if (problemStatuses.has(result.status))
106
+ issues.push({ code: `cache-${result.status}`, severity: 'error',
107
+ message: result.reason, paths: [result.path] });
108
+ }
109
+ }
110
+ const counts = {
111
+ adopted: 0, reusable: 0, planned: 0, missing: 0, corrupt: 0, unavailable: 0, conflict: 0,
112
+ };
113
+ for (const record of records)
114
+ counts[record.status]++;
115
+ return {
116
+ schemaVersion: 1, kind: 'semantic-cache-adoption', repositoryId: config.repositoryId,
117
+ ref, commitSha, configurationDigest: sha256(stableJson(config)),
118
+ previousConfigurationDigest: sha256(stableJson(previous.config)), dryRun,
119
+ ready: complete && !issues.some((issue) => issue.severity === 'error'), scope: { ...scope, complete },
120
+ records, counts, selections: selection.selections, budgets: selection.budgets, issues,
121
+ assumptions: [
122
+ 'Selection uses the same configured or bounded agent scope as hydration planning at the resolved commit; only expected keys in the explicitly named previous profile are inspected.',
123
+ 'Missing or incomplete previous artifacts need later extraction. Corrupt or conflicting complete artifacts are retained for diagnosis.',
124
+ 'Adoption preserves original artifact identity and provenance; it does not validate or publish a current snapshot.',
125
+ 'Artifacts are adopted independently; earlier valid copies remain available if a later artifact or storage operation fails.',
126
+ 'No provider, authentication, indexing, ontology or repository adapter is initialized. Dry runs do not write cache files.',
127
+ ],
128
+ };
129
+ }
@@ -0,0 +1,6 @@
1
+ import type { ContextGraphConfig } from '../configuration/config.js';
2
+ export declare const graphAssemblyVersion = 7;
3
+ export declare const graphAssemblyIdentity = "standalone-graph-v7";
4
+ /** Runtime expectations are separate from source/configuration identity and artifact reuse. */
5
+ export declare function requiredSnapshotExtractorVersions(config?: Pick<ContextGraphConfig, 'routingSources'>): Readonly<Record<string, string>>;
6
+ export declare function incompatibleSnapshotExtractors(actual: Readonly<Record<string, string>> | undefined, expected: Readonly<Record<string, string>>): readonly string[];
@@ -0,0 +1,15 @@
1
+ import { repositoryContractExtractorIdentity, scipSymbolExtractorIdentity } from '../extraction/static.js';
2
+ import { routingExtractorIdentity } from '../extraction/routing.js';
3
+ export const graphAssemblyVersion = 7;
4
+ export const graphAssemblyIdentity = `standalone-graph-v${graphAssemblyVersion}`;
5
+ /** Runtime expectations are separate from source/configuration identity and artifact reuse. */
6
+ export function requiredSnapshotExtractorVersions(config) {
7
+ return { assembly: graphAssemblyIdentity,
8
+ [repositoryContractExtractorIdentity.id]: repositoryContractExtractorIdentity.version,
9
+ [scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
10
+ ...(config?.routingSources?.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}),
11
+ };
12
+ }
13
+ export function incompatibleSnapshotExtractors(actual, expected) {
14
+ return Object.keys(expected).filter((id) => actual?.[id] !== expected[id]).sort();
15
+ }
@@ -0,0 +1,41 @@
1
+ import { SemanticProviderError } from '../extraction/semantic-process.js';
2
+ import { SemanticPolicyError, type SemanticHydrationPhase } from '../extraction/semantic-diagnostics.js';
3
+ export type SemanticFailureCode = SemanticProviderError['code'] | SemanticPolicyError['code'] | 'cache-failure' | 'provider-failure';
4
+ export interface SemanticRunProgress {
5
+ readonly total: number;
6
+ readonly completed: number;
7
+ readonly extracted: number;
8
+ readonly reused: number;
9
+ readonly failed: number;
10
+ readonly cancelled: number;
11
+ readonly notStarted: number;
12
+ /** Includes failed, cancelled and not-started sources; it is not an extraction estimate. */
13
+ readonly remaining: number;
14
+ }
15
+ export interface SemanticHydrationFailure {
16
+ readonly schemaVersion: 1;
17
+ readonly kind: 'semantic-hydration-failure';
18
+ readonly code: SemanticFailureCode;
19
+ readonly category: 'timeout' | 'cancellation' | 'provider' | 'infrastructure' | 'extraction-gap';
20
+ readonly phase: SemanticHydrationPhase | 'semantic-scheduling';
21
+ /** Absent when cancellation occurs between sources, without one failing source. */
22
+ readonly sourceId?: string;
23
+ readonly commitSha: string;
24
+ readonly configurationDigest: string;
25
+ /** Processing time through failure, including cache checks/retries for a source. */
26
+ readonly elapsedMs: number;
27
+ readonly elapsedScope: 'source' | 'semantic-run';
28
+ readonly timeoutMs: number;
29
+ readonly timeoutScope: 'provider-attempt' | 'module-execution';
30
+ readonly progress: SemanticRunProgress;
31
+ readonly forced: boolean;
32
+ readonly recovery: readonly string[];
33
+ }
34
+ export declare function semanticFailureCode(error: unknown, phase: SemanticHydrationPhase, signal?: AbortSignal): SemanticFailureCode;
35
+ export declare function semanticFailureCategory(code: SemanticFailureCode): SemanticHydrationFailure['category'];
36
+ /** Public failure envelope deliberately excludes raw errors, causes and provider output. */
37
+ export declare class SemanticHydrationError extends Error {
38
+ readonly report: SemanticHydrationFailure;
39
+ readonly code: SemanticFailureCode;
40
+ constructor(report: SemanticHydrationFailure);
41
+ }
@@ -0,0 +1,43 @@
1
+ import { SemanticProviderError } from '../extraction/semantic-process.js';
2
+ import { SemanticPolicyError } from '../extraction/semantic-diagnostics.js';
3
+ export function semanticFailureCode(error, phase, signal) {
4
+ if (signal?.aborted && (error === signal.reason ||
5
+ (error instanceof Error && error.name === 'AbortError') ||
6
+ (phase === 'semantic-extract' && !(error instanceof SemanticProviderError))))
7
+ return 'cancelled';
8
+ if (error instanceof SemanticProviderError &&
9
+ ['timeout', 'cancelled', 'unavailable', 'exit', 'invalid-output'].includes(error.code))
10
+ return error.code;
11
+ if (error instanceof SemanticPolicyError && ['unresolved', 'low-confidence'].includes(error.code))
12
+ return error.code;
13
+ if (phase === 'semantic-cache-read' || phase === 'semantic-cache-write')
14
+ return 'cache-failure';
15
+ return phase === 'semantic-validate' ? 'invalid-output' : 'provider-failure';
16
+ }
17
+ export function semanticFailureCategory(code) {
18
+ if (code === 'timeout')
19
+ return 'timeout';
20
+ if (code === 'cancelled')
21
+ return 'cancellation';
22
+ if (code === 'unavailable' || code === 'cache-failure')
23
+ return 'infrastructure';
24
+ if (code === 'invalid-output' || code === 'unresolved' || code === 'low-confidence')
25
+ return 'extraction-gap';
26
+ return 'provider';
27
+ }
28
+ /** Public failure envelope deliberately excludes raw errors, causes and provider output. */
29
+ export class SemanticHydrationError extends Error {
30
+ report;
31
+ code;
32
+ constructor(report) {
33
+ const p = report.progress;
34
+ super(`Semantic hydration ${report.code}: ${report.sourceId ?? 'between semantic sources'}; phase ${report.phase}; ` +
35
+ `${report.elapsedScope} elapsed ${report.elapsedMs} ms; configured ${report.timeoutScope} timeout ${report.timeoutMs} ms. ` +
36
+ `${p.completed}/${p.total} completed (${p.extracted} extracted, ${p.reused} reused); ` +
37
+ `${p.remaining} remaining (${p.failed} failed, ${p.cancelled} cancelled, ${p.notStarted} not started).\n` +
38
+ report.recovery.join('\n'));
39
+ this.report = report;
40
+ this.name = 'SemanticHydrationError';
41
+ this.code = report.code;
42
+ }
43
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ImpactScanRequest } from '@weftlet/core';
2
2
  import { type SemanticCacheInspection } from '../extraction/semantic-cache.js';
3
3
  import { type SemanticInputIssue, type SemanticSelection } from '../extraction/semantic-sources.js';
4
+ import { type SemanticScope } from './semantic-scope.js';
4
5
  export interface HydrationPlanOptions {
5
6
  readonly ref?: string;
6
7
  readonly forceSemantic?: boolean;
@@ -8,6 +9,8 @@ export interface HydrationPlanOptions {
8
9
  }
9
10
  export interface HydrationPlanDocument {
10
11
  readonly path: string;
12
+ /** Local mapping for opaque IDs in semantic progress and failure reports. */
13
+ readonly sourceId: string;
11
14
  readonly stableSemanticId: string;
12
15
  readonly sourceDigest: string;
13
16
  readonly bytes: number;
@@ -27,14 +30,8 @@ export interface HydrationPlan {
27
30
  readonly commitSha: string;
28
31
  readonly configurationDigest: string;
29
32
  readonly ready: boolean;
30
- readonly scope: {
31
- readonly mode: 'configured' | 'agent';
32
- readonly selectors: readonly string[];
33
- readonly digest: string;
34
- readonly headName: string;
33
+ readonly scope: SemanticScope & {
35
34
  readonly complete: boolean;
36
- readonly omittedDocuments: readonly string[];
37
- readonly ignoredSearchTerms: number;
38
35
  };
39
36
  readonly documents: readonly HydrationPlanDocument[];
40
37
  readonly selections: readonly SemanticSelection[];
@@ -1,42 +1,34 @@
1
1
  import { loadConfig } from '../configuration/config.js';
2
- import { resolveStorageRoot } from '../configuration/cache.js';
3
2
  import { GitRepository } from '../repository/git.js';
4
3
  import { matchesRepositoryGlob } from '../repository/glob.js';
5
- import { SemanticArtifactStore } from '../storage/storage.js';
4
+ import { createSemanticArtifactStore } from './semantic-artifact-store.js';
6
5
  import { inspectSemanticCache } from '../extraction/semantic-cache.js';
7
6
  import { semanticProviderIdentity } from '../extraction/semantic-identity.js';
8
7
  import { selectSemanticInputs } from '../extraction/semantic-sources.js';
9
8
  import { sha256, stableJson } from '../infrastructure/util.js';
10
- import { AgentSemanticScopeResolver } from './agent-context.js';
9
+ import { resolveSemanticScope } from './semantic-scope.js';
10
+ import { semanticSourceId } from '../extraction/semantic-diagnostics.js';
11
11
  async function describeDocument(source, selections, config, evidencePaths, store, forceSemantic = false) {
12
12
  const inspection = forceSemantic
13
13
  ? { status: 'forced', reason: 'Explicit force bypasses existing artifacts; a new revision is allocated only during hydration.' }
14
14
  : await inspectSemanticCache(store, config.semantic, source, semanticProviderIdentity(config.semantic));
15
15
  const { artifact: _artifact, ...cache } = inspection;
16
16
  const approvedBy = (config.approvedIntentSources ?? []).filter((pattern) => matchesRepositoryGlob(source.path, pattern));
17
- return { path: source.path, stableSemanticId: source.stableSemanticId, sourceDigest: source.digest,
17
+ return { path: source.path, sourceId: semanticSourceId(config.repositoryId, source),
18
+ stableSemanticId: source.stableSemanticId, sourceDigest: source.digest,
18
19
  bytes: Buffer.byteLength(source.content), lines: source.content.split('\n').length,
19
20
  selectedBy: selections.filter((item) => item.paths.includes(source.path)).map((item) => item.selector),
20
21
  role: evidencePaths.has(source.path) ? 'validation-evidence' : 'semantic-document',
21
22
  sourceAuthority: approvedBy.length ? 'declared' : 'inferred', approvedBy,
22
23
  extractionReview: 'unreviewed', cache };
23
24
  }
24
- async function resolvePlanningScope(config, repository, commitSha, request = {}) {
25
- const agent = config.agentContext
26
- ? await new AgentSemanticScopeResolver(config, repository).resolve(request, commitSha)
27
- : undefined;
28
- const selectors = agent?.semanticSources ?? config.semanticSources;
29
- return { mode: agent ? 'agent' : 'configured', selectors, digest: sha256(stableJson(selectors)),
30
- headName: agent?.headName ?? 'default', omittedDocuments: agent?.omittedDocuments ?? [],
31
- ignoredSearchTerms: agent?.ignoredSearchTerms ?? 0 };
32
- }
33
25
  /** Inspect only committed source objects, config and existing artifacts; never initialize a provider or adapter. */
34
26
  export async function planHydration(configPath, options = {}) {
35
27
  const { config, repositoryRoot } = await loadConfig(configPath);
36
28
  const repository = new GitRepository(repositoryRoot);
37
29
  const ref = options.ref ?? 'HEAD';
38
30
  const commitSha = await repository.resolveRef(ref);
39
- const scope = await resolvePlanningScope(config, repository, commitSha, options.request);
31
+ const scope = await resolveSemanticScope(config, repository, commitSha, options.request);
40
32
  const selection = await selectSemanticInputs(repository, commitSha, scope.selectors, config.semantic);
41
33
  const issues = [...selection.issues];
42
34
  const { omittedDocuments, ignoredSearchTerms } = scope;
@@ -46,7 +38,7 @@ export async function planHydration(configPath, options = {}) {
46
38
  issues.push({ code: 'selection-limited', severity: 'warning',
47
39
  message: `Existing agent selection limits omitted ${omittedDocuments.length} documents and ${ignoredSearchTerms} search terms; this plan describes only the selected scope.`,
48
40
  paths: omittedDocuments });
49
- const store = new SemanticArtifactStore(resolveStorageRoot(repositoryRoot, config));
41
+ const store = createSemanticArtifactStore(repositoryRoot, config);
50
42
  const evidencePaths = new Set(await repository.listFilesAt(commitSha, config.evidenceRoots));
51
43
  const documents = [];
52
44
  for (const source of selection.sources) {
@@ -4,13 +4,11 @@ import type { GitRepository } from "../repository/git.js";
4
4
  import type { OntologyCompiler } from "../extraction/ontology.js";
5
5
  import type { SemanticHydrator } from "../extraction/semantic.js";
6
6
  import type { ScipIndexResult, StaticIndexer } from "../extraction/static.js";
7
+ import { type HydrationProgressEvent } from './semantic-run.js';
8
+ export type { HydrationProgressEvent } from './semantic-run.js';
7
9
  export interface HydrationOptions {
8
10
  readonly signal?: AbortSignal;
9
- readonly onProgress?: (event: {
10
- phase: string;
11
- completed?: number;
12
- total?: number;
13
- }) => void;
11
+ readonly onProgress?: (event: HydrationProgressEvent) => void;
14
12
  readonly ref: string;
15
13
  readonly headName: string;
16
14
  readonly forceSemantic?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { scipSourcePatterns } from '../configuration/scip-project.js';
2
2
  import { contractPatternRoots } from "../repository/glob.js";
3
- import { repositoryFileExtractorIdentity } from '../extraction/static.js';
3
+ import { repositoryFileExtractorIdentity, repositoryContractExtractorIdentity, scipSymbolExtractorIdentity } from '../extraction/static.js';
4
4
  import { sha256, stableJson } from "../infrastructure/util.js";
5
5
  import { documentLinkerIdentity, linkSemanticDocuments } from "../extraction/document.js";
6
6
  import { repositoryNodeIdentityVersion } from '../extraction/repository-nodes.js';
@@ -9,27 +9,9 @@ import { runtimeEvidenceFacts } from "../extraction/runtime-evidence.js";
9
9
  import { matchesRepositoryGlob } from "../repository/glob.js";
10
10
  import { RdfEvidenceGraph } from "../graph/rdf.js";
11
11
  import { extractRoutingDeclarations, routingExtractorIdentity } from '../extraction/routing.js';
12
- async function concurrentMap(values, concurrency, worker) {
13
- const results = new Array(values.length);
14
- let cursor = 0;
15
- let failure;
16
- let failed = false;
17
- await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, values.length)) }, async () => {
18
- while (!failed && cursor < values.length) {
19
- const index = cursor++;
20
- try {
21
- results[index] = await worker(values[index], index);
22
- }
23
- catch (error) {
24
- failed = true;
25
- failure ??= error;
26
- }
27
- }
28
- }));
29
- if (failed)
30
- throw failure;
31
- return results;
32
- }
12
+ import { SemanticProviderError } from '../extraction/semantic-process.js';
13
+ import { hydrateSemanticSources } from './semantic-run.js';
14
+ import { graphAssemblyVersion, graphAssemblyIdentity } from './extraction-identity.js';
33
15
  export class ContextGraphHydrator {
34
16
  dependencies;
35
17
  constructor(dependencies) {
@@ -57,7 +39,11 @@ export class ContextGraphHydrator {
57
39
  };
58
40
  }
59
41
  async hydrate(options) {
60
- options.signal?.throwIfAborted();
42
+ const checkpoint = () => {
43
+ if (options.signal?.aborted)
44
+ throw new SemanticProviderError('cancelled', 'Hydration cancelled');
45
+ };
46
+ checkpoint();
61
47
  options.onProgress?.({ phase: 'resolve-committed-sources' });
62
48
  const { config, repository, snapshotStore } = this.dependencies;
63
49
  const commitSha = await repository.resolveRef(options.ref);
@@ -99,7 +85,9 @@ export class ContextGraphHydrator {
99
85
  : undefined;
100
86
  const configurationDigest = sha256(stableJson(config));
101
87
  const scopeDigest = sha256(stableJson(selectedSemanticSources)).slice(0, 12);
102
- const effectiveSuffix = ['graph-v6', `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
88
+ const effectiveSuffix = [`graph-v${graphAssemblyVersion}`, `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
89
+ `contracts-${repositoryContractExtractorIdentity.version}`,
90
+ `symbols-${scipSymbolExtractorIdentity.version}`,
103
91
  ...(config.routingSources?.length ? [`routes-${routingExtractorIdentity.version}`] : []),
104
92
  configurationDigest.slice(0, 16), scopeDigest, baseSuffix, runtimeSuffix].filter(Boolean).join('-');
105
93
  const snapshotId = effectiveSuffix ? `${commitSha}-${effectiveSuffix}` : commitSha;
@@ -117,26 +105,10 @@ export class ContextGraphHydrator {
117
105
  };
118
106
  // Validate committed routing inputs before invoking any provider or indexer.
119
107
  const routing = await extractRoutingDeclarations(repository, context, config.routingSources ?? []);
120
- let semanticExtracted = 0;
121
- let semanticReused = 0;
122
- const controller = new AbortController();
123
- const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
124
- const hydrated = await concurrentMap(sources, config.semantic.concurrency, async (source) => {
125
- let result;
126
- try {
127
- result = await this.dependencies.semanticHydrator.hydrate(source, context, forceRevision, { signal });
128
- }
129
- catch (error) {
130
- controller.abort();
131
- throw error;
132
- }
133
- if (result.reused)
134
- semanticReused += 1;
135
- else
136
- semanticExtracted += 1;
137
- options.onProgress?.({ phase: result.reused ? 'semantic-cache-hit' : 'semantic-extracted',
138
- completed: semanticReused + semanticExtracted, total: sources.length });
139
- return { source, ...result };
108
+ const { hydrated, semanticExtracted, semanticReused } = await hydrateSemanticSources({
109
+ sources, context, config: config.semantic, configurationDigest,
110
+ hydrator: this.dependencies.semanticHydrator, forceRevision,
111
+ signal: options.signal, onProgress: options.onProgress,
140
112
  });
141
113
  const artifacts = hydrated.map((result) => result.artifact);
142
114
  const diagnostics = hydrated.flatMap(({ source, artifact }) => (artifact.diagnostics ?? []).map((item) => ({
@@ -163,7 +135,7 @@ export class ContextGraphHydrator {
163
135
  extractionReview: 'unreviewed',
164
136
  sourceAuthority: (config.approvedIntentSources?.some((pattern) => matchesRepositoryGlob(source.path, pattern)) ? 'declared' : 'inferred'),
165
137
  })));
166
- options.signal?.throwIfAborted();
138
+ checkpoint();
167
139
  options.onProgress?.({ phase: 'index-typescript' });
168
140
  const scip = await this.dependencies.staticIndexer.index(context);
169
141
  const extractorFacts = (await Promise.all(this.dependencies.extractorFactory(scip).map((extractor) => extractor.extract(context)))).flat();
@@ -184,7 +156,7 @@ export class ContextGraphHydrator {
184
156
  const documents = linkSemanticDocuments(context, hydrated, trackedPaths, sources.filter((source) => config.approvedIntentSources?.some((pattern) => matchesRepositoryGlob(source.path, pattern))).map((source) => source.path));
185
157
  diagnostics.push(...documents.diagnostics);
186
158
  diagnostics.push(...routing.diagnostics);
187
- options.signal?.throwIfAborted();
159
+ checkpoint();
188
160
  options.onProgress?.({ phase: 'compile-semantic-rdf' });
189
161
  const ontology = await this.dependencies.ontologyCompiler.compile(snapshotId, artifacts, context);
190
162
  const runtimeEvidence = runtimeRevision.references;
@@ -230,7 +202,9 @@ export class ContextGraphHydrator {
230
202
  scope: selectedSemanticSources,
231
203
  ontologyVersion: config.semantic.ontologyVersion,
232
204
  extractorVersions: {
233
- assembly: 'standalone-graph-v6',
205
+ assembly: graphAssemblyIdentity,
206
+ [repositoryContractExtractorIdentity.id]: repositoryContractExtractorIdentity.version,
207
+ [scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
234
208
  'repository-node-identity': String(repositoryNodeIdentityVersion),
235
209
  [documentLinkerIdentity.id]: documentLinkerIdentity.version,
236
210
  ...(config.routingSources?.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}),
@@ -256,12 +230,13 @@ export class ContextGraphHydrator {
256
230
  unresolved,
257
231
  diagnostics,
258
232
  };
259
- options.signal?.throwIfAborted();
233
+ checkpoint();
260
234
  options.onProgress?.({ phase: 'validate-shacl' });
261
235
  const validation = await new RdfEvidenceGraph(snapshot).validate();
262
236
  if (!validation.conforms) {
263
237
  throw new Error(`Graph extraction failed SHACL validation: ${JSON.stringify(validation.issues)}`);
264
238
  }
239
+ checkpoint();
265
240
  options.onProgress?.({ phase: 'publish-snapshot' });
266
241
  await snapshotStore.write(snapshot);
267
242
  await snapshotStore.updateHead(options.headName, snapshotId);