@weftlet/toolkit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,71 @@ the archive-byte, snapshot replacement and MCP startup regression suite also pas
67
67
  Windows CI. Full native Windows release preparation remains separately unvalidated.
68
68
  See the [validation record](docs/VALIDATION.md).
69
69
 
70
+ ## Preview hydration before model work
71
+
72
+ `hydrate --plan` requires toolkit **0.3.0 or newer**. Releases through 0.2.1 do not support
73
+ this flag. With a supporting version installed, inspect a project's committed inputs:
74
+
75
+ ```sh
76
+ pnpm exec weftlet hydrate --plan --config /path/to/project/context-graph.config.json --ref HEAD
77
+ pnpm exec weftlet hydrate --plan --json --config /path/to/project/context-graph.config.json --ref HEAD
78
+ ```
79
+
80
+ The human summary and JSON identify the resolved commit, current local configuration, selected
81
+ documents and inclusion reasons, UTF-8 byte sizes, source approval, expected cache reuse and
82
+ extraction counts, budget violations, and working changes excluded from the committed input.
83
+ Selection uses configured repository file/directory paths; `approvedIntentSources` uses its
84
+ separate glob matcher. An explicit empty semantic source list selects no documents, fixing the
85
+ earlier accidental expansion to the whole repository.
86
+
87
+ Planning reads Git objects and existing artifacts without loading a model provider or repository
88
+ adapter, indexing code, collecting runtime evidence, validating the graph or writing snapshots.
89
+ It does not check credentials or estimate tokens or dollars. Cache hits depend on unchanged
90
+ inputs; corrupt or unreadable entries are reported separately from misses. `--force-semantic`
91
+ shows planned re-extraction without allocating or writing a forced revision.
92
+
93
+ A plan exits 0 when its source/cache/budget checks are ready. It exits 1 with the full plan when
94
+ errors block those checks or incomplete selection needs review. Missing/no-document selectors
95
+ and existing agent-selection limits are warnings; they do not add a new hydration gate that
96
+ would prevent removing the last document and invalidating its relationships. All selected
97
+ documents remain visible when budgets are exceeded.
98
+
99
+ Source approval does not approve model assertions. Validation-document labels reflect configured
100
+ evidence roots; selected validation documents still undergo semantic extraction today. Recursive
101
+ SOT traversal and default validation-document separation remain T07 work. See
102
+ [ADR 016](docs/ADR-016-hydration-preview.md) and the [1.0 task list](docs/RELEASE-1.0-PLAN.md).
103
+
104
+ ## SHACL validation and performance
105
+
106
+ Starting with 0.3.0, the toolkit indexes a temporary union to accelerate cold validation.
107
+ The toolkit uses JavaScript `rdf-validate-shacl` and an in-memory
108
+ N3 store. Each hydration validates the complete graph; semantic artifact reuse does not make
109
+ validation incremental, and there is no validation-result cache, Rust engine or GraphDB service.
110
+ Saved queries do not rerun SHACL.
111
+
112
+ The current shapes use subject/predicate/object values independently of graph names. Validation
113
+ now builds one temporary union graph so repeated property lookups avoid scanning every named
114
+ assertion graph. The same shapes and validator run against that union. The original named dataset,
115
+ assertion records and provenance remain the source for SPARQL, queries and serialization.
116
+
117
+ In bounded, single-run public experiments, 2,000 facts with unique subject/object nodes took
118
+ 31.50 seconds in the original SHACL path, compared with 0.153 seconds to build the union and
119
+ 0.467 seconds for SHACL on that union. A 10,000-fact union case used about 1.3 GiB process RSS;
120
+ the extra projection has a memory cost. These measurements do not reproduce the private consumer's
121
+ full generation run or guarantee a production duration. See [ADR 017](docs/ADR-017-shacl-validation-union.md)
122
+ for methodology, parity boundaries and remaining work. To run the bounded public benchmark from
123
+ a development checkout:
124
+
125
+ ```sh
126
+ pnpm build
127
+ pnpm benchmark:shacl
128
+ ```
129
+
130
+ Current hydration progress reports phase boundaries. The validator performs synchronous work,
131
+ so an ordinary timer cannot provide an honest heartbeat while that work blocks Node's main
132
+ thread. Worker-based progress, early identity checks, validation-result reuse and incremental
133
+ validation remain separate T05 work; they do not delay the focused cold-validation release.
134
+
70
135
  ## Development
71
136
 
72
137
  The development source is organized by responsibility; see
@@ -7,12 +7,14 @@ export interface AgentSemanticScope {
7
7
  readonly semanticSources: readonly string[];
8
8
  readonly headName: string;
9
9
  readonly snapshotSuffix: string;
10
+ readonly omittedDocuments?: readonly string[];
11
+ readonly ignoredSearchTerms?: number;
10
12
  }
11
13
  export declare class AgentSemanticScopeResolver {
12
14
  private readonly config;
13
15
  private readonly repository;
14
16
  constructor(config: ContextGraphConfig, repository: GitRepository);
15
- resolve(request: ImpactScanRequest): Promise<AgentSemanticScope>;
17
+ resolve(request: ImpactScanRequest, ref?: string): Promise<AgentSemanticScope>;
16
18
  }
17
19
  export declare class PreparedImpactQueryService implements ImpactQueryService {
18
20
  private readonly store;
@@ -16,13 +16,15 @@ export class AgentSemanticScopeResolver {
16
16
  this.config = config;
17
17
  this.repository = repository;
18
18
  }
19
- async resolve(request) {
19
+ async resolve(request, ref = 'HEAD') {
20
20
  const agent = this.config.agentContext;
21
21
  if (!agent)
22
22
  throw new Error('Agent context preparation is not configured');
23
- const commitSha = await this.repository.resolveRef('HEAD');
23
+ const commitSha = await this.repository.resolveRef(ref);
24
24
  const available = new Set((await this.repository.listFilesAt(commitSha, this.config.semanticSources)).filter((path) => /\.(?:md|mdx|txt)$/i.test(path)));
25
25
  const selected = new Set();
26
+ const omitted = new Set();
27
+ let ignoredSearchTerms = 0;
26
28
  for (const path of await this.repository.listFilesAt(commitSha, agent.alwaysSemanticSources)) {
27
29
  if (available.has(path))
28
30
  selected.add(path);
@@ -42,6 +44,8 @@ export class AgentSemanticScopeResolver {
42
44
  .filter((path) => available.has(path));
43
45
  for (const path of references.slice(0, agent.maxReferencedDocuments))
44
46
  selected.add(path);
47
+ for (const path of references.slice(agent.maxReferencedDocuments))
48
+ omitted.add(path);
45
49
  }
46
50
  if (agent.includeMatchingEvidence) {
47
51
  const ownerSlug = basename(ownerDocument).replace(/\.md$/i, '');
@@ -51,16 +55,19 @@ export class AgentSemanticScopeResolver {
51
55
  }
52
56
  }
53
57
  else {
54
- const terms = [
58
+ const requestedTerms = [
55
59
  request.task,
56
60
  ...(request.checklistItems ?? []),
57
61
  ...(request.capabilities ?? []),
58
62
  ]
59
- .filter((term) => Boolean(term))
60
- .slice(0, 8);
61
- const matches = await this.repository.findTextAt(commitSha, terms, this.config.semanticSources);
63
+ .filter((term) => Boolean(term));
64
+ const terms = requestedTerms.slice(0, 8);
65
+ ignoredSearchTerms = requestedTerms.length - terms.length;
66
+ const matches = (await this.repository.findTextAt(commitSha, terms, this.config.semanticSources)).filter((match) => available.has(match.path));
62
67
  for (const match of matches.slice(0, agent.maxReferencedDocuments))
63
68
  selected.add(match.path);
69
+ for (const match of matches.slice(agent.maxReferencedDocuments))
70
+ omitted.add(match.path);
64
71
  }
65
72
  const semanticSources = [...selected].sort();
66
73
  if (semanticSources.length === 0) {
@@ -72,6 +79,8 @@ export class AgentSemanticScopeResolver {
72
79
  semanticSources,
73
80
  headName: `agent-${scopeDigest}`,
74
81
  snapshotSuffix: `agent-${scopeDigest}`,
82
+ omittedDocuments: [...omitted].filter((path) => !selected.has(path)).sort(),
83
+ ignoredSearchTerms,
75
84
  };
76
85
  }
77
86
  }
@@ -0,0 +1,57 @@
1
+ import type { ImpactScanRequest } from '@weftlet/core';
2
+ import { type SemanticCacheInspection } from '../extraction/semantic-cache.js';
3
+ import { type SemanticInputIssue, type SemanticSelection } from '../extraction/semantic-sources.js';
4
+ export interface HydrationPlanOptions {
5
+ readonly ref?: string;
6
+ readonly forceSemantic?: boolean;
7
+ readonly request?: ImpactScanRequest;
8
+ }
9
+ export interface HydrationPlanDocument {
10
+ readonly path: string;
11
+ readonly stableSemanticId: string;
12
+ readonly sourceDigest: string;
13
+ readonly bytes: number;
14
+ readonly lines: number;
15
+ readonly selectedBy: readonly string[];
16
+ readonly role: 'semantic-document' | 'validation-evidence';
17
+ readonly sourceAuthority: 'declared' | 'inferred';
18
+ readonly approvedBy: readonly string[];
19
+ readonly extractionReview: 'unreviewed';
20
+ readonly cache: Omit<SemanticCacheInspection, 'artifact'>;
21
+ }
22
+ export interface HydrationPlan {
23
+ readonly schemaVersion: 1;
24
+ readonly kind: 'hydration-plan';
25
+ readonly repositoryId: string;
26
+ readonly ref: string;
27
+ readonly commitSha: string;
28
+ readonly configurationDigest: string;
29
+ readonly ready: boolean;
30
+ readonly scope: {
31
+ readonly mode: 'configured' | 'agent';
32
+ readonly selectors: readonly string[];
33
+ readonly digest: string;
34
+ readonly headName: string;
35
+ readonly complete: boolean;
36
+ readonly omittedDocuments: readonly string[];
37
+ readonly ignoredSearchTerms: number;
38
+ };
39
+ readonly documents: readonly HydrationPlanDocument[];
40
+ readonly selections: readonly SemanticSelection[];
41
+ readonly budgets: {
42
+ readonly maxDocuments: number;
43
+ readonly maxSourceBytes: number;
44
+ readonly selectedDocuments: number;
45
+ readonly totalSourceBytes: number;
46
+ };
47
+ readonly estimates: {
48
+ readonly reusableArtifacts: number;
49
+ readonly extractions: number;
50
+ readonly unknownArtifacts: number;
51
+ };
52
+ readonly issues: readonly SemanticInputIssue[];
53
+ readonly excludedWorkingChanges: readonly string[];
54
+ readonly assumptions: readonly string[];
55
+ }
56
+ /** Inspect only committed source objects, config and existing artifacts; never initialize a provider or adapter. */
57
+ export declare function planHydration(configPath: string, options?: HydrationPlanOptions): Promise<HydrationPlan>;
@@ -0,0 +1,79 @@
1
+ import { loadConfig } from '../configuration/config.js';
2
+ import { resolveStorageRoot } from '../configuration/cache.js';
3
+ import { GitRepository } from '../repository/git.js';
4
+ import { matchesRepositoryGlob } from '../repository/glob.js';
5
+ import { SemanticArtifactStore } from '../storage/storage.js';
6
+ import { inspectSemanticCache } from '../extraction/semantic-cache.js';
7
+ import { semanticProviderIdentity } from '../extraction/semantic-identity.js';
8
+ import { selectSemanticInputs } from '../extraction/semantic-sources.js';
9
+ import { sha256, stableJson } from '../infrastructure/util.js';
10
+ import { AgentSemanticScopeResolver } from './agent-context.js';
11
+ async function describeDocument(source, selections, config, evidencePaths, store, forceSemantic = false) {
12
+ const inspection = forceSemantic
13
+ ? { status: 'forced', reason: 'Explicit force bypasses existing artifacts; a new revision is allocated only during hydration.' }
14
+ : await inspectSemanticCache(store, config.semantic, source, semanticProviderIdentity(config.semantic));
15
+ const { artifact: _artifact, ...cache } = inspection;
16
+ const approvedBy = (config.approvedIntentSources ?? []).filter((pattern) => matchesRepositoryGlob(source.path, pattern));
17
+ return { path: source.path, stableSemanticId: source.stableSemanticId, sourceDigest: source.digest,
18
+ bytes: Buffer.byteLength(source.content), lines: source.content.split('\n').length,
19
+ selectedBy: selections.filter((item) => item.paths.includes(source.path)).map((item) => item.selector),
20
+ role: evidencePaths.has(source.path) ? 'validation-evidence' : 'semantic-document',
21
+ sourceAuthority: approvedBy.length ? 'declared' : 'inferred', approvedBy,
22
+ extractionReview: 'unreviewed', cache };
23
+ }
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
+ /** Inspect only committed source objects, config and existing artifacts; never initialize a provider or adapter. */
34
+ export async function planHydration(configPath, options = {}) {
35
+ const { config, repositoryRoot } = await loadConfig(configPath);
36
+ const repository = new GitRepository(repositoryRoot);
37
+ const ref = options.ref ?? 'HEAD';
38
+ const commitSha = await repository.resolveRef(ref);
39
+ const scope = await resolvePlanningScope(config, repository, commitSha, options.request);
40
+ const selection = await selectSemanticInputs(repository, commitSha, scope.selectors, config.semantic);
41
+ const issues = [...selection.issues];
42
+ const { omittedDocuments, ignoredSearchTerms } = scope;
43
+ const limited = omittedDocuments.length > 0 || ignoredSearchTerms > 0;
44
+ const complete = !limited && selection.selections.every((item) => item.status === 'matched');
45
+ if (limited)
46
+ issues.push({ code: 'selection-limited', severity: 'warning',
47
+ message: `Existing agent selection limits omitted ${omittedDocuments.length} documents and ${ignoredSearchTerms} search terms; this plan describes only the selected scope.`,
48
+ paths: omittedDocuments });
49
+ const store = new SemanticArtifactStore(resolveStorageRoot(repositoryRoot, config));
50
+ const evidencePaths = new Set(await repository.listFilesAt(commitSha, config.evidenceRoots));
51
+ const documents = [];
52
+ for (const source of selection.sources) {
53
+ const document = await describeDocument(source, selection.selections, config, evidencePaths, store, options.forceSemantic);
54
+ const { cache } = document;
55
+ if (cache.status === 'corrupt' || cache.status === 'unavailable')
56
+ issues.push({
57
+ code: `cache-${cache.status}`, severity: 'error', message: cache.reason, paths: [source.path],
58
+ });
59
+ documents.push(document);
60
+ }
61
+ const count = (...states) => documents.filter((doc) => states.includes(doc.cache.status)).length;
62
+ return {
63
+ schemaVersion: 1, kind: 'hydration-plan', repositoryId: config.repositoryId, ref, commitSha,
64
+ configurationDigest: sha256(stableJson(config)), ready: complete && !issues.some((item) => item.severity === 'error'),
65
+ scope: { ...scope, complete },
66
+ documents, selections: selection.selections, budgets: selection.budgets,
67
+ estimates: { reusableArtifacts: count('hit'), extractions: count('miss', 'forced'), unknownArtifacts: count('corrupt', 'unavailable') },
68
+ issues, excludedWorkingChanges: await repository.workingChanges(),
69
+ assumptions: [
70
+ 'Sources come from the resolved commit; configuration and cache are read from the current local environment.',
71
+ 'Estimates require unchanged source, configuration, provider contract and readable artifacts; concurrent changes can invalidate them.',
72
+ 'Document and per-document UTF-8 byte limits apply to all selected documents, including cache hits; no token or monetary estimate is made.',
73
+ 'Ready describes semantic inputs and readable cache only. Providers, credentials, adapters, storage writes, indexing, runtime evidence, routing, ontology and graph validation have not been checked.',
74
+ 'Roles reflect configured evidence roots, not inferred prose meaning. Selected validation documents still undergo semantic extraction today; source approval never approves extracted assertions.',
75
+ 'Selection is the configured scope, optionally narrowed by existing bounded agent selection; recursive SOT traversal and complete product coverage are not claimed.',
76
+ 'Unmatched selectors and bounded scope omissions need review. They do not introduce new hydration gates: removing the last document can still publish a graph with its relationships removed.',
77
+ ],
78
+ };
79
+ }
@@ -4,6 +4,7 @@ import { repositoryFileExtractorIdentity } 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';
7
+ import { selectSemanticInputs } from '../extraction/semantic-sources.js';
7
8
  import { runtimeEvidenceFacts } from "../extraction/runtime-evidence.js";
8
9
  import { matchesRepositoryGlob } from "../repository/glob.js";
9
10
  import { RdfEvidenceGraph } from "../graph/rdf.js";
@@ -62,12 +63,17 @@ export class ContextGraphHydrator {
62
63
  const commitSha = await repository.resolveRef(options.ref);
63
64
  if (options.requireClean === true)
64
65
  await repository.requireExactCleanCommit(commitSha);
66
+ const selectedSemanticSources = options.semanticSources ?? config.semanticSources;
67
+ const selection = await selectSemanticInputs(repository, commitSha, selectedSemanticSources, config.semantic);
68
+ if (selection.issues.some((issue) => issue.severity === 'error')) {
69
+ throw new Error(selection.issues.map((issue) => issue.message).join('\n'));
70
+ }
71
+ const { sources } = selection;
65
72
  await this.dependencies.ontologyCompiler.checkReady?.();
66
73
  const prior = await snapshotStore.readHead(options.headName);
67
74
  const baseSha = prior?.manifest.commitSha === commitSha
68
75
  ? prior.manifest.baseSha
69
76
  : (prior?.manifest.commitSha ?? (await repository.firstParent(commitSha)));
70
- const selectedSemanticSources = options.semanticSources ?? config.semanticSources;
71
77
  const allRoots = [
72
78
  ...new Set([
73
79
  ...selectedSemanticSources,
@@ -93,7 +99,7 @@ export class ContextGraphHydrator {
93
99
  : undefined;
94
100
  const configurationDigest = sha256(stableJson(config));
95
101
  const scopeDigest = sha256(stableJson(selectedSemanticSources)).slice(0, 12);
96
- const effectiveSuffix = ['graph-v5', `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
102
+ const effectiveSuffix = ['graph-v6', `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
97
103
  ...(config.routingSources?.length ? [`routes-${routingExtractorIdentity.version}`] : []),
98
104
  configurationDigest.slice(0, 16), scopeDigest, baseSuffix, runtimeSuffix].filter(Boolean).join('-');
99
105
  const snapshotId = effectiveSuffix ? `${commitSha}-${effectiveSuffix}` : commitSha;
@@ -111,18 +117,6 @@ export class ContextGraphHydrator {
111
117
  };
112
118
  // Validate committed routing inputs before invoking any provider or indexer.
113
119
  const routing = await extractRoutingDeclarations(repository, context, config.routingSources ?? []);
114
- const sources = await repository.semanticSources(context, selectedSemanticSources);
115
- if (sources.length > (config.semantic.maxDocuments ?? 8) ||
116
- sources.some((source) => Buffer.byteLength(source.content) > (config.semantic.maxSourceBytes ?? 65_536))) {
117
- throw new Error('Semantic source budget exceeded before extraction; narrow scope or explicitly review configured limits');
118
- }
119
- const ids = new Set();
120
- for (const source of sources) {
121
- if (ids.has(source.stableSemanticId)) {
122
- throw new Error(`Duplicate stable semantic ID: ${source.stableSemanticId}`);
123
- }
124
- ids.add(source.stableSemanticId);
125
- }
126
120
  let semanticExtracted = 0;
127
121
  let semanticReused = 0;
128
122
  const controller = new AbortController();
@@ -236,7 +230,7 @@ export class ContextGraphHydrator {
236
230
  scope: selectedSemanticSources,
237
231
  ontologyVersion: config.semantic.ontologyVersion,
238
232
  extractorVersions: {
239
- assembly: 'standalone-graph-v5',
233
+ assembly: 'standalone-graph-v6',
240
234
  'repository-node-identity': String(repositoryNodeIdentityVersion),
241
235
  [documentLinkerIdentity.id]: documentLinkerIdentity.version,
242
236
  ...(config.routingSources?.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}),
@@ -1,12 +1,12 @@
1
1
  import { CodexCliSemanticExtractor, SemanticProviderError } from "./semantic.js";
2
- import { sha256, stableJson } from "../infrastructure/util.js";
2
+ import { semanticProviderIdentity } from './semantic-identity.js';
3
3
  import { resolveConsumerModule } from "../infrastructure/module-resolution.js";
4
4
  /** Lazy loading ensures a missing provider cannot disable saved-evidence queries. */
5
5
  export function createSemanticExtractor(config, repositoryRoot) {
6
6
  if (config.provider === 'codex-cli')
7
7
  return new CodexCliSemanticExtractor(config);
8
8
  const modulePath = config.providerModule;
9
- const identity = `${config.providerIdentity}:${sha256(stableJson(config))}:contract-v2`;
9
+ const identity = semanticProviderIdentity(config);
10
10
  let extractor;
11
11
  return {
12
12
  identity,
@@ -0,0 +1,12 @@
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
+ }
11
+ /** Read-only; a broken artifact is not an extraction miss or permission to overwrite evidence. */
12
+ export declare function inspectSemanticCache(store: SemanticArtifactStore, config: ContextGraphConfig['semantic'], source: SemanticSource, modelIdentity: string, forceRevision?: string): Promise<SemanticCacheInspection>;
@@ -0,0 +1,59 @@
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
+ try {
46
+ value = await store.read(key);
47
+ }
48
+ catch (error) {
49
+ return error instanceof SyntaxError
50
+ ? { status: 'corrupt', key, reason: 'Cached artifact is not valid JSON; retain it for diagnosis or use an explicit forced refresh.' }
51
+ : { status: 'unavailable', key, reason: 'Cached artifact could not be read; check local storage access before hydrating.' };
52
+ }
53
+ if (value === undefined)
54
+ return { status: 'miss', key, reason: 'No artifact exists for the current source and extraction contract.' };
55
+ if (!reusableArtifact(value, key, source, config, modelIdentity)) {
56
+ 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
+ }
58
+ return { status: 'hit', key, reason: 'Readable artifact matches the source and extraction contract; reuse remains subject to unchanged inputs.', artifact: value };
59
+ }
@@ -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
+ }
@@ -8,7 +8,9 @@ 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';
12
14
  import { assertSemanticCitation, numberedSemanticSource, semanticCitationSchema, } from "./semantic-citations.js";
13
15
  function canonicalizeRepositoryDocuments(output) {
14
16
  const ids = new Map();
@@ -228,7 +230,7 @@ export class CodexCliSemanticExtractor {
228
230
  this.config = config;
229
231
  this.runner = runner;
230
232
  this.loginProbe = loginProbe;
231
- this.identity = `codex-cli:${config.model}:${config.reasoningEffort}:${config.promptVersion}:contract-v3`;
233
+ this.identity = semanticProviderIdentity(config);
232
234
  }
233
235
  async extract(source, context, options = {}) {
234
236
  options.signal?.throwIfAborted();
@@ -333,24 +335,12 @@ export class SemanticHydrator {
333
335
  }
334
336
  async hydrate(source, context, forceRevision, options = {}) {
335
337
  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,
349
- };
350
- const key = sha256(stableJson(keyInput));
351
- const existing = await this.store.read(key);
352
- if (existing)
353
- return { artifact: existing, reused: true };
338
+ const key = semanticArtifactKey(this.config, source, this.extractor.identity, forceRevision);
339
+ const cached = await inspectSemanticCache(this.store, this.config, source, this.extractor.identity, forceRevision);
340
+ if (cached.status === 'hit')
341
+ return { artifact: cached.artifact, reused: true };
342
+ if (cached.status !== 'miss')
343
+ throw new Error(cached.reason);
354
344
  const extraction = await this.extractor.extract(source, context, options);
355
345
  assertSemanticOutput(extraction.parsed, source);
356
346
  if (typeof extraction.rawModelOutput !== 'string' || Buffer.byteLength(extraction.rawModelOutput) > 2 * 1024 * 1024) {
@@ -411,7 +401,7 @@ export class SemanticHydrator {
411
401
  throw new Error(`Unresolved semantic mappings for ${source.path}: ${[...unresolvedMappings, ...diagnostics.map((item) => item.reason)].join('; ')}`);
412
402
  }
413
403
  const compiledOml = renderOml(key, output.assertions);
414
- const outputDigest = sha256(stableJson({ facts, diagnostics, compiledOml, rawModelOutput: extraction.rawModelOutput }));
404
+ const outputDigest = semanticArtifactOutputDigest({ facts, diagnostics, oml: compiledOml, rawModelOutput: extraction.rawModelOutput });
415
405
  const artifact = {
416
406
  key,
417
407
  stableSemanticId: source.stableSemanticId,
@@ -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. */