@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 +65 -0
- package/dist/application/agent-context.d.ts +3 -1
- package/dist/application/agent-context.js +15 -6
- package/dist/application/hydration-plan.d.ts +57 -0
- package/dist/application/hydration-plan.js +79 -0
- package/dist/application/hydrator.js +9 -15
- package/dist/extraction/providers.js +2 -2
- package/dist/extraction/semantic-cache.d.ts +12 -0
- package/dist/extraction/semantic-cache.js +59 -0
- package/dist/extraction/semantic-identity.d.ts +7 -0
- package/dist/extraction/semantic-identity.js +29 -0
- package/dist/extraction/semantic-sources.d.ts +27 -0
- package/dist/extraction/semantic-sources.js +35 -0
- package/dist/extraction/semantic.js +11 -21
- package/dist/graph/rdf.d.ts +1 -1
- package/dist/graph/rdf.js +13 -28
- package/dist/graph/shacl-shapes.d.ts +5 -0
- package/dist/graph/shacl-shapes.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interfaces/cli.js +47 -3
- package/dist/repository/git.d.ts +1 -1
- package/dist/repository/git.js +5 -1
- package/docs/ADR-016-hydration-preview.md +91 -0
- package/docs/ADR-017-shacl-validation-union.md +102 -0
- package/docs/RELEASE-1.0-PLAN.md +96 -29
- package/docs/RELEASING.md +17 -6
- package/docs/ROADMAP.md +27 -5
- package/docs/VALIDATION.md +133 -3
- package/docs/WEFTLET-2.0-PLAN.md +9 -8
- package/docs/extraction-manifest.json +21 -10
- package/package.json +3 -2
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(
|
|
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
|
|
58
|
+
const requestedTerms = [
|
|
55
59
|
request.task,
|
|
56
60
|
...(request.checklistItems ?? []),
|
|
57
61
|
...(request.capabilities ?? []),
|
|
58
62
|
]
|
|
59
|
-
.filter((term) => Boolean(term))
|
|
60
|
-
|
|
61
|
-
|
|
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-
|
|
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-
|
|
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 {
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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 =
|
|
404
|
+
const outputDigest = semanticArtifactOutputDigest({ facts, diagnostics, oml: compiledOml, rawModelOutput: extraction.rawModelOutput });
|
|
415
405
|
const artifact = {
|
|
416
406
|
key,
|
|
417
407
|
stableSemanticId: source.stableSemanticId,
|
package/dist/graph/rdf.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphFact, GraphSnapshot } from "@weftlet/core";
|
|
2
|
-
export
|
|
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. */
|