@weftlet/toolkit 0.4.0 → 0.6.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 +149 -14
- package/dist/application/cache-adoption.d.ts +2 -1
- package/dist/application/cache-adoption.js +12 -5
- package/dist/application/document-coverage.d.ts +28 -0
- package/dist/application/document-coverage.js +58 -0
- package/dist/application/document-overlay.d.ts +14 -0
- package/dist/application/document-overlay.js +31 -0
- package/dist/application/extraction-identity.d.ts +2 -1
- package/dist/application/extraction-identity.js +7 -0
- package/dist/application/hydration-inputs.d.ts +15 -0
- package/dist/application/hydration-inputs.js +55 -0
- package/dist/application/hydration-plan.d.ts +7 -0
- package/dist/application/hydration-plan.js +18 -5
- package/dist/application/hydrator.d.ts +2 -0
- package/dist/application/hydrator.js +40 -18
- package/dist/application/overlay.d.ts +3 -1
- package/dist/application/overlay.js +29 -9
- package/dist/application/saved-evidence.d.ts +2 -2
- package/dist/application/semantic-run.d.ts +3 -0
- package/dist/application/semantic-scope.d.ts +1 -1
- package/dist/application/semantic-scope.js +2 -2
- package/dist/configuration/cache.js +13 -1
- package/dist/configuration/config.d.ts +2 -0
- package/dist/configuration/config.js +6 -1
- package/dist/configuration/document-traversal.d.ts +7 -0
- package/dist/configuration/document-traversal.js +13 -0
- package/dist/extraction/canonical-document-links.d.ts +28 -0
- package/dist/extraction/canonical-document-links.js +95 -0
- package/dist/extraction/document-classification.d.ts +44 -0
- package/dist/extraction/document-classification.js +161 -0
- package/dist/extraction/document-evidence.d.ts +21 -0
- package/dist/extraction/document-evidence.js +83 -0
- package/dist/extraction/document-traversal.d.ts +76 -0
- package/dist/extraction/document-traversal.js +210 -0
- package/dist/extraction/document.d.ts +3 -1
- package/dist/extraction/document.js +3 -1
- package/dist/graph/authority.js +3 -0
- package/dist/graph/coverage.d.ts +8 -0
- package/dist/graph/coverage.js +96 -0
- package/dist/graph/document-role-description.d.ts +5 -0
- package/dist/graph/document-role-description.js +18 -0
- package/dist/graph/identity-preflight.d.ts +57 -0
- package/dist/graph/identity-preflight.js +71 -0
- package/dist/graph/query.d.ts +2 -2
- package/dist/graph/query.js +22 -62
- package/dist/graph/rdf.d.ts +4 -1
- package/dist/graph/rdf.js +21 -6
- package/dist/graph/shacl-shapes.js +26 -0
- package/dist/graph/validation-progress.d.ts +14 -0
- package/dist/graph/validation-progress.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/interfaces/cli.js +29 -5
- package/dist/interfaces/document-coverage.d.ts +9 -0
- package/dist/interfaces/document-coverage.js +117 -0
- package/dist/repository/git.d.ts +11 -0
- package/dist/repository/git.js +38 -0
- package/dist/visualization/visualization.js +21 -6
- package/docs/ADR-017-shacl-validation-union.md +4 -0
- package/docs/ADR-018-operational-semantic-cache.md +2 -2
- package/docs/ADR-019-semantic-failure-diagnostics.md +2 -2
- package/docs/ADR-020-canonical-physical-file-producers.md +2 -2
- package/docs/ADR-021-identity-preflight-and-validation-timing.md +84 -0
- package/docs/ADR-022-release-documentation.md +44 -0
- package/docs/ADR-023-document-coverage-preview.md +64 -0
- package/docs/ADR-024-implementation-coverage.md +51 -0
- package/docs/ADR-025-document-classification.md +78 -0
- package/docs/ADR-026-traversal-hydration.md +94 -0
- package/docs/ADR-027-validation-closeout.md +155 -0
- package/docs/CONSUMER-TRIAL.md +1 -1
- package/docs/MIGRATING-TO-WEFTLET.md +1 -1
- package/docs/PROVIDERS.md +17 -1
- package/docs/RELEASE-1.0-PLAN.md +177 -48
- package/docs/RELEASING.md +46 -8
- package/docs/ROADMAP.md +59 -28
- package/docs/VALIDATION.md +451 -0
- package/docs/benchmarks/validation-2026-10-28/README.md +25 -0
- package/docs/benchmarks/validation-2026-10-28/large.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/lifecycle.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/native.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/rich.json +1 -0
- package/docs/extraction-manifest.json +73 -17
- package/examples/generic-typescript/document-policy.json +9 -0
- package/package.json +7 -4
|
@@ -4,14 +4,16 @@ import { repositoryFileExtractorIdentity, repositoryContractExtractorIdentity, s
|
|
|
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 {
|
|
7
|
+
import { selectHydrationInputs, classifyHydratedFacts, documentSelectionIdentity } from './hydration-inputs.js';
|
|
8
8
|
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
12
|
import { SemanticProviderError } from '../extraction/semantic-process.js';
|
|
13
13
|
import { hydrateSemanticSources } from './semantic-run.js';
|
|
14
|
-
import { graphAssemblyVersion, graphAssemblyIdentity } from './extraction-identity.js';
|
|
14
|
+
import { graphAssemblyVersion, graphAssemblyIdentity, documentExtractionVersions } from './extraction-identity.js';
|
|
15
|
+
import { assertGraphIdentities } from '../graph/identity-preflight.js';
|
|
16
|
+
import { measureValidationPhase } from '../graph/validation-progress.js';
|
|
15
17
|
export class ContextGraphHydrator {
|
|
16
18
|
dependencies;
|
|
17
19
|
constructor(dependencies) {
|
|
@@ -49,10 +51,16 @@ export class ContextGraphHydrator {
|
|
|
49
51
|
const commitSha = await repository.resolveRef(options.ref);
|
|
50
52
|
if (options.requireClean === true)
|
|
51
53
|
await repository.requireExactCleanCommit(commitSha);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
if (config.documentTraversal && options.semanticSources !== undefined) {
|
|
55
|
+
throw new Error('Traversal hydration does not accept a direct semanticSources override');
|
|
56
|
+
}
|
|
57
|
+
const selection = await selectHydrationInputs(repository, commitSha, config, options.semanticSources);
|
|
58
|
+
const selectedSemanticSources = selection.documentSelection?.semanticPaths ?? options.semanticSources ?? config.semanticSources;
|
|
59
|
+
const scopePaths = selection.documentSelection?.documentPaths ?? selectedSemanticSources;
|
|
60
|
+
if (selection.documentSelection?.complete === false || selection.issues.some((issue) => issue.severity === 'error')) {
|
|
61
|
+
throw new Error(config.documentTraversal
|
|
62
|
+
? 'Traversal hydration blocked: run hydrate --plan --json to review coverage, roles and budgets before retrying.'
|
|
63
|
+
: selection.issues.map((issue) => issue.message).join('\n'));
|
|
56
64
|
}
|
|
57
65
|
const { sources } = selection;
|
|
58
66
|
await this.dependencies.ontologyCompiler.checkReady?.();
|
|
@@ -62,7 +70,7 @@ export class ContextGraphHydrator {
|
|
|
62
70
|
: (prior?.manifest.commitSha ?? (await repository.firstParent(commitSha)));
|
|
63
71
|
const allRoots = [
|
|
64
72
|
...new Set([
|
|
65
|
-
...
|
|
73
|
+
...scopePaths,
|
|
66
74
|
...config.codeRoots,
|
|
67
75
|
...config.evidenceRoots,
|
|
68
76
|
...contractPatternRoots(config.contractPatterns),
|
|
@@ -84,11 +92,12 @@ export class ContextGraphHydrator {
|
|
|
84
92
|
? `runtime-${runtimeRevision.digest.slice(0, 12)}`
|
|
85
93
|
: undefined;
|
|
86
94
|
const configurationDigest = sha256(stableJson(config));
|
|
87
|
-
const scopeDigest = sha256(stableJson(
|
|
95
|
+
const scopeDigest = sha256(stableJson(scopePaths)).slice(0, 12);
|
|
88
96
|
const effectiveSuffix = [`graph-v${graphAssemblyVersion}`, `links-${documentLinkerIdentity.version}`, `files-${repositoryFileExtractorIdentity.version}`,
|
|
89
97
|
`contracts-${repositoryContractExtractorIdentity.version}`,
|
|
90
98
|
`symbols-${scipSymbolExtractorIdentity.version}`,
|
|
91
99
|
...(config.routingSources?.length ? [`routes-${routingExtractorIdentity.version}`] : []),
|
|
100
|
+
...(selection.documentSelection ? [documentSelectionIdentity, selection.documentSelection.digest.slice(0, 16)] : []),
|
|
92
101
|
configurationDigest.slice(0, 16), scopeDigest, baseSuffix, runtimeSuffix].filter(Boolean).join('-');
|
|
93
102
|
const snapshotId = effectiveSuffix ? `${commitSha}-${effectiveSuffix}` : commitSha;
|
|
94
103
|
const context = {
|
|
@@ -121,7 +130,7 @@ export class ContextGraphHydrator {
|
|
|
121
130
|
commitSha,
|
|
122
131
|
})),
|
|
123
132
|
})));
|
|
124
|
-
const semanticFacts = hydrated.flatMap(({ source, artifact }) => artifact.facts.map((fact) => ({
|
|
133
|
+
const semanticFacts = classifyHydratedFacts(hydrated.flatMap(({ source, artifact }) => artifact.facts.map((fact) => ({
|
|
125
134
|
...fact,
|
|
126
135
|
id: sha256(stableJson({ artifact: artifact.key, fact: fact.id, path: source.path })),
|
|
127
136
|
provenance: {
|
|
@@ -134,7 +143,7 @@ export class ContextGraphHydrator {
|
|
|
134
143
|
authority: 'inferred',
|
|
135
144
|
extractionReview: 'unreviewed',
|
|
136
145
|
sourceAuthority: (config.approvedIntentSources?.some((pattern) => matchesRepositoryGlob(source.path, pattern)) ? 'declared' : 'inferred'),
|
|
137
|
-
})));
|
|
146
|
+
}))), selection.documentEvidence);
|
|
138
147
|
checkpoint();
|
|
139
148
|
options.onProgress?.({ phase: 'index-typescript' });
|
|
140
149
|
const scip = await this.dependencies.staticIndexer.index(context);
|
|
@@ -153,15 +162,25 @@ export class ContextGraphHydrator {
|
|
|
153
162
|
});
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
const documents = linkSemanticDocuments(context, hydrated, trackedPaths, sources.filter((source) => config.approvedIntentSources?.some((pattern) => matchesRepositoryGlob(source.path, pattern))).map((source) => source.path));
|
|
165
|
+
const documents = linkSemanticDocuments(context, hydrated, trackedPaths, sources.filter((source) => config.approvedIntentSources?.some((pattern) => matchesRepositoryGlob(source.path, pattern))).map((source) => source.path), { canonicalReferences: !config.documentTraversal });
|
|
157
166
|
diagnostics.push(...documents.diagnostics);
|
|
158
|
-
diagnostics.push(...routing.diagnostics);
|
|
167
|
+
diagnostics.push(...routing.diagnostics, ...(selection.documentEvidence?.diagnostics ?? []));
|
|
159
168
|
checkpoint();
|
|
160
|
-
options.onProgress?.({ phase: 'compile-semantic-rdf' });
|
|
161
|
-
const ontology = await this.dependencies.ontologyCompiler.compile(snapshotId, artifacts, context);
|
|
162
169
|
const runtimeEvidence = runtimeRevision.references;
|
|
163
170
|
const runtimeFacts = runtimeEvidenceFacts(context, runtimeEvidence);
|
|
164
|
-
const
|
|
171
|
+
const structuralFacts = selection.documentEvidence?.facts.map(fact => ({ ...fact, snapshotId })) ?? [];
|
|
172
|
+
const facts = [...semanticFacts, ...classifyHydratedFacts(documents.facts, selection.documentEvidence),
|
|
173
|
+
...structuralFacts, ...extractorFacts, ...routing.facts, ...runtimeFacts].sort((left, right) => left.id.localeCompare(right.id));
|
|
174
|
+
const validationCounts = { factCount: facts.length };
|
|
175
|
+
await measureValidationPhase('validate-identities', validationCounts, options.onProgress, () => {
|
|
176
|
+
const report = assertGraphIdentities(facts, {
|
|
177
|
+
repositoryId: config.repositoryId, includeDetails: options.validationDetails,
|
|
178
|
+
});
|
|
179
|
+
validationCounts.nodeCount = report.nodeCount;
|
|
180
|
+
});
|
|
181
|
+
checkpoint();
|
|
182
|
+
options.onProgress?.({ phase: 'compile-semantic-rdf' });
|
|
183
|
+
const ontology = await this.dependencies.ontologyCompiler.compile(snapshotId, artifacts, context);
|
|
165
184
|
const invariants = (await Promise.all(this.dependencies.invariantEvaluators.map((evaluator) => evaluator.evaluate(snapshotId, facts)))).flat();
|
|
166
185
|
const failures = invariants.filter((invariant) => invariant.status === 'fail');
|
|
167
186
|
if (failures.length > 0) {
|
|
@@ -188,6 +207,7 @@ export class ContextGraphHydrator {
|
|
|
188
207
|
unresolved,
|
|
189
208
|
diagnostics,
|
|
190
209
|
semanticSources: selectedSemanticSources,
|
|
210
|
+
...(selection.documentSelection ? { documentSelection: selection.documentSelection } : {}),
|
|
191
211
|
};
|
|
192
212
|
const contentDigest = sha256(stableJson(logical));
|
|
193
213
|
const snapshot = {
|
|
@@ -199,10 +219,12 @@ export class ContextGraphHydrator {
|
|
|
199
219
|
baseSha,
|
|
200
220
|
snapshotId,
|
|
201
221
|
configurationDigest,
|
|
202
|
-
scope:
|
|
222
|
+
scope: scopePaths,
|
|
223
|
+
...(selection.documentSelection ? { documentSelection: selection.documentSelection } : {}),
|
|
203
224
|
ontologyVersion: config.semantic.ontologyVersion,
|
|
204
225
|
extractorVersions: {
|
|
205
226
|
assembly: graphAssemblyIdentity,
|
|
227
|
+
...documentExtractionVersions(config),
|
|
206
228
|
[repositoryContractExtractorIdentity.id]: repositoryContractExtractorIdentity.version,
|
|
207
229
|
[scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
|
|
208
230
|
'repository-node-identity': String(repositoryNodeIdentityVersion),
|
|
@@ -231,8 +253,8 @@ export class ContextGraphHydrator {
|
|
|
231
253
|
diagnostics,
|
|
232
254
|
};
|
|
233
255
|
checkpoint();
|
|
234
|
-
options.onProgress
|
|
235
|
-
const validation = await
|
|
256
|
+
const graph = await measureValidationPhase('build-rdf-evidence', validationCounts, options.onProgress, () => new RdfEvidenceGraph(snapshot));
|
|
257
|
+
const validation = await graph.validate({ onProgress: (event) => options.onProgress?.({ ...event, nodeCount: validationCounts.nodeCount }) });
|
|
236
258
|
if (!validation.conforms) {
|
|
237
259
|
throw new Error(`Graph extraction failed SHACL validation: ${JSON.stringify(validation.issues)}`);
|
|
238
260
|
}
|
|
@@ -3,4 +3,6 @@ import type { GraphSnapshot } from "@weftlet/core";
|
|
|
3
3
|
import { GitRepository } from "../repository/git.js";
|
|
4
4
|
import { FileSnapshotStore } from "../storage/storage.js";
|
|
5
5
|
/** Conservative code overlay: reindex implementation; invalidate changed prose, never synthesize intent. */
|
|
6
|
-
export declare function buildWorkingOverlay(repository: GitRepository, config: ContextGraphConfig, store: FileSnapshotStore, storageRoot: string, baseSnapshotId: string, agent?: string
|
|
6
|
+
export declare function buildWorkingOverlay(repository: GitRepository, config: ContextGraphConfig, store: FileSnapshotStore, storageRoot: string, baseSnapshotId: string, agent?: string, options?: {
|
|
7
|
+
readonly validationDetails?: boolean;
|
|
8
|
+
}): Promise<GraphSnapshot>;
|
|
@@ -11,14 +11,18 @@ import { contextPackEvidenceAuthority } from "../graph/query.js";
|
|
|
11
11
|
import { inWorkingScope, readWorkingContent } from "../repository/working-content.js";
|
|
12
12
|
import { extractRoutingDeclarations, routingExtractorIdentity } from '../extraction/routing.js';
|
|
13
13
|
import { RdfEvidenceGraph } from '../graph/rdf.js';
|
|
14
|
-
import { graphAssemblyIdentity } from './extraction-identity.js';
|
|
14
|
+
import { graphAssemblyIdentity, documentExtractionVersions } from './extraction-identity.js';
|
|
15
|
+
import { assertGraphIdentities } from '../graph/identity-preflight.js';
|
|
16
|
+
import { selectHydrationInputs, documentSelectionIdentity } from './hydration-inputs.js';
|
|
17
|
+
import { retainOverlayDocumentFact, overlayDocumentRoots, overlayDocumentManifest, overlayDocumentNotes } from './document-overlay.js';
|
|
18
|
+
import { documentEvidenceExtractorIdentity } from '../extraction/document-evidence.js';
|
|
15
19
|
const execute = promisify(execFile);
|
|
16
20
|
const implementationExtractors = new Set([
|
|
17
21
|
'repository-file-extractor', 'repository-contract-extractor', 'scip-symbol-extractor',
|
|
18
22
|
routingExtractorIdentity.id,
|
|
19
23
|
]);
|
|
20
24
|
/** Conservative code overlay: reindex implementation; invalidate changed prose, never synthesize intent. */
|
|
21
|
-
export async function buildWorkingOverlay(repository, config, store, storageRoot, baseSnapshotId, agent = 'default') {
|
|
25
|
+
export async function buildWorkingOverlay(repository, config, store, storageRoot, baseSnapshotId, agent = 'default', options = {}) {
|
|
22
26
|
const base = await store.read(baseSnapshotId);
|
|
23
27
|
if (!base || base.manifest.overlayDigest)
|
|
24
28
|
throw new Error('Overlay requires a committed base snapshot');
|
|
@@ -38,6 +42,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
38
42
|
commitSha: head, snapshotId: baseSnapshotId, changedPaths: [], deletedPaths: [], renamedPaths: {},
|
|
39
43
|
}, routingSources, { allowMissingSources: true });
|
|
40
44
|
const roots = [...config.codeRoots, ...config.semanticSources, ...config.evidenceRoots,
|
|
45
|
+
...overlayDocumentRoots(config),
|
|
41
46
|
...config.contractPatterns, ...scipSourcePatterns(config.scip.sourceProject),
|
|
42
47
|
...routingSources, ...baseRouting.referencedPaths];
|
|
43
48
|
const excluded = [];
|
|
@@ -63,6 +68,8 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
63
68
|
repositoryContractExtractor: repositoryContractExtractorIdentity,
|
|
64
69
|
scipSymbolExtractor: scipSymbolExtractorIdentity,
|
|
65
70
|
...(routingSources.length ? { routingExtractor: routingExtractorIdentity } : {}),
|
|
71
|
+
...(config.documentTraversal ? { documentSelection: documentSelectionIdentity,
|
|
72
|
+
documentEvidence: documentEvidenceExtractorIdentity } : {}),
|
|
66
73
|
})).slice(0, 32)}`;
|
|
67
74
|
const affected = new Set(captured.map((file) => file.path));
|
|
68
75
|
const extracted = await withCommittedTree(repository.root, head, async (root) => {
|
|
@@ -89,6 +96,13 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
89
96
|
deletedPaths: captured.filter((f) => f.content === null).map((f) => f.path),
|
|
90
97
|
renamedPaths: {},
|
|
91
98
|
};
|
|
99
|
+
const documents = config.documentTraversal
|
|
100
|
+
? await selectHydrationInputs(draftRepository, context.commitSha, config) : undefined;
|
|
101
|
+
const documentSelection = documents?.documentSelection ? {
|
|
102
|
+
...documents.documentSelection,
|
|
103
|
+
// Temporary Git commit timestamps are not evidence identity. Bind drafts to captured bytes.
|
|
104
|
+
digest: sha256(stableJson({ ...documents.documentSelection, digest: undefined, baseSha: head, overlayDigest })),
|
|
105
|
+
} : undefined;
|
|
92
106
|
const scip = await new ScipTypeScriptIndexer(config.scip, storageRoot).index(context);
|
|
93
107
|
const routing = await extractRoutingDeclarations(draftRepository, context, routingSources, { allowMissingSources: true });
|
|
94
108
|
const uncapturedEndpoints = routing.referencedPaths.filter((path) => !inWorkingScope(path, roots));
|
|
@@ -101,36 +115,41 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
101
115
|
new ScipSymbolExtractor(draftRepository, scip),
|
|
102
116
|
];
|
|
103
117
|
const facts = [...(await Promise.all(extractors.map((e) => e.extract(context)))).flat(),
|
|
104
|
-
...routing.facts].map(({ id: _id, ...fact }) => {
|
|
118
|
+
...routing.facts, ...(documents?.documentEvidence?.facts ?? [])].map(({ id: _id, ...fact }) => {
|
|
105
119
|
const draft = { ...fact, snapshotId, authority: 'draft',
|
|
106
|
-
...(fact.extractor.id === routingExtractorIdentity.id ? { sourceAuthority: 'draft' } : {}),
|
|
120
|
+
...(fact.extractor.id === routingExtractorIdentity.id || fact.sourceRole !== undefined ? { sourceAuthority: 'draft' } : {}),
|
|
107
121
|
provenance: { ...fact.provenance, commitSha: head } };
|
|
108
122
|
return { ...draft, id: sha256(stableJson(draft)) };
|
|
109
123
|
});
|
|
110
|
-
const diagnostics = routing.diagnostics.map(({ id: _id, ...item }) => {
|
|
124
|
+
const diagnostics = [...routing.diagnostics, ...(documents?.documentEvidence?.diagnostics ?? [])].map(({ id: _id, ...item }) => {
|
|
111
125
|
const draft = { ...item, provenance: item.provenance.map((span) => ({ ...span, commitSha: head })) };
|
|
112
126
|
return { ...draft, id: sha256(stableJson(draft)) };
|
|
113
127
|
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
114
|
-
return { facts, diagnostics };
|
|
128
|
+
return { facts, diagnostics, documents, documentSelection };
|
|
115
129
|
});
|
|
116
|
-
const retained = base.facts.filter((fact) =>
|
|
130
|
+
const retained = base.facts.filter((fact) => retainOverlayDocumentFact(fact, extracted.documents, affected) &&
|
|
131
|
+
!implementationExtractors.has(fact.extractor.id) &&
|
|
117
132
|
contextPackEvidenceAuthority(fact) !== 'implemented' &&
|
|
118
133
|
fact.extractor.id !== 'otlp-runtime-evidence' &&
|
|
119
134
|
!affected.has(fact.provenance.path) &&
|
|
120
135
|
![fact.subject, fact.object].some((node) => [...affected].some((path) => node.id === `component:file:${path}` || node.id === `document:${path}` || node.label === path))).map((fact) => ({ ...fact, snapshotId }));
|
|
121
136
|
const allFacts = [...retained, ...extracted.facts].sort((a, b) => a.id.localeCompare(b.id));
|
|
137
|
+
assertGraphIdentities(allFacts, { repositoryId: config.repositoryId, includeDetails: options.validationDetails });
|
|
122
138
|
const overlay = {
|
|
123
139
|
...base,
|
|
124
140
|
manifest: {
|
|
125
141
|
...base.manifest, snapshotId, baseSha: head, overlayDigest, overlayCapture,
|
|
142
|
+
...overlayDocumentManifest(extracted.documentSelection),
|
|
126
143
|
extractorVersions: { ...base.manifest.extractorVersions,
|
|
127
144
|
assembly: graphAssemblyIdentity,
|
|
145
|
+
...documentExtractionVersions(config),
|
|
128
146
|
[repositoryFileExtractorIdentity.id]: repositoryFileExtractorIdentity.version,
|
|
129
147
|
[repositoryContractExtractorIdentity.id]: repositoryContractExtractorIdentity.version,
|
|
130
148
|
[scipSymbolExtractorIdentity.id]: scipSymbolExtractorIdentity.version,
|
|
131
149
|
...(routingSources.length ? { [routingExtractorIdentity.id]: routingExtractorIdentity.version } : {}) },
|
|
132
|
-
generatedAt: new Date().toISOString(), contentDigest: sha256(stableJson(routingSources.length
|
|
133
|
-
? { facts: allFacts, diagnostics: extracted.diagnostics
|
|
150
|
+
generatedAt: new Date().toISOString(), contentDigest: sha256(stableJson(routingSources.length || config.documentTraversal
|
|
151
|
+
? { facts: allFacts, diagnostics: extracted.diagnostics,
|
|
152
|
+
...(extracted.documentSelection ? { documentSelection: extracted.documentSelection } : {}) } : allFacts)),
|
|
134
153
|
},
|
|
135
154
|
facts: allFacts,
|
|
136
155
|
invariants: base.invariants.map((rule) => ({
|
|
@@ -139,6 +158,7 @@ export async function buildWorkingOverlay(repository, config, store, storageRoot
|
|
|
139
158
|
runtimeEvidence: [],
|
|
140
159
|
unresolved: [
|
|
141
160
|
'Draft implementation overlay: changed semantic documents require explicit re-extraction.',
|
|
161
|
+
...overlayDocumentNotes(config),
|
|
142
162
|
`Changes outside configured source scope are excluded: ${excluded.join(', ') || '(none)'}`,
|
|
143
163
|
],
|
|
144
164
|
diagnostics: extracted.diagnostics,
|
|
@@ -15,13 +15,13 @@ export declare class SavedEvidenceQueryService implements ImpactQueryService {
|
|
|
15
15
|
incompatibleExtractors?: readonly string[] | undefined;
|
|
16
16
|
stale: boolean;
|
|
17
17
|
configurationMatches: boolean;
|
|
18
|
-
overlayFreshness?: "
|
|
18
|
+
overlayFreshness?: "unknown" | "current" | "stale" | undefined;
|
|
19
19
|
capturedWorkingChanges?: string[] | undefined;
|
|
20
20
|
changedSinceCapture?: string[] | undefined;
|
|
21
21
|
currentCommit: string | undefined;
|
|
22
22
|
excludedWorkingChanges: string[];
|
|
23
23
|
};
|
|
24
|
-
snapshot: Pick<import("@weftlet/core").SnapshotManifest, "snapshotId" | "commitSha" | "contentDigest" | "configurationDigest" | "scope" | "baseSha" | "overlayDigest" | "overlayCapture"> & Partial<Pick<import("@weftlet/core").SnapshotManifest, "extractorVersions">>;
|
|
24
|
+
snapshot: Pick<import("@weftlet/core").SnapshotManifest, "snapshotId" | "commitSha" | "contentDigest" | "configurationDigest" | "scope" | "baseSha" | "overlayDigest" | "overlayCapture"> & Partial<Pick<import("@weftlet/core").SnapshotManifest, "extractorVersions" | "documentSelection">>;
|
|
25
25
|
capabilities: readonly import("@weftlet/core").GraphNode[];
|
|
26
26
|
facts: readonly import("@weftlet/core").GraphFact[];
|
|
27
27
|
invariants: readonly import("@weftlet/core").InvariantResult[];
|
|
@@ -7,6 +7,9 @@ export interface HydrationProgressEvent {
|
|
|
7
7
|
readonly total?: number;
|
|
8
8
|
readonly sourceId?: string;
|
|
9
9
|
readonly elapsedMs?: number;
|
|
10
|
+
readonly status?: 'started' | 'completed' | 'failed';
|
|
11
|
+
readonly factCount?: number;
|
|
12
|
+
readonly nodeCount?: number;
|
|
10
13
|
}
|
|
11
14
|
/** Settle active workers before reporting failure; never publish snapshots here. */
|
|
12
15
|
export declare function hydrateSemanticSources(options: {
|
|
@@ -2,7 +2,7 @@ import type { ImpactScanRequest } from '@weftlet/core';
|
|
|
2
2
|
import type { ContextGraphConfig } from '../configuration/config.js';
|
|
3
3
|
import type { GitRepository } from '../repository/git.js';
|
|
4
4
|
export interface SemanticScope {
|
|
5
|
-
readonly mode: 'configured' | 'agent';
|
|
5
|
+
readonly mode: 'configured' | 'agent' | 'traversal';
|
|
6
6
|
readonly selectors: readonly string[];
|
|
7
7
|
readonly digest: string;
|
|
8
8
|
readonly headName: string;
|
|
@@ -5,8 +5,8 @@ export async function resolveSemanticScope(config, repository, commitSha, reques
|
|
|
5
5
|
const agent = config.agentContext
|
|
6
6
|
? await new AgentSemanticScopeResolver(config, repository).resolve(request, commitSha)
|
|
7
7
|
: undefined;
|
|
8
|
-
const selectors = agent?.semanticSources ?? config.semanticSources;
|
|
9
|
-
return { mode: agent ? 'agent' : 'configured', selectors, digest: sha256(stableJson(selectors)),
|
|
8
|
+
const selectors = config.documentTraversal?.entryPoints ?? agent?.semanticSources ?? config.semanticSources;
|
|
9
|
+
return { mode: config.documentTraversal ? 'traversal' : agent ? 'agent' : 'configured', selectors, digest: sha256(stableJson(selectors)),
|
|
10
10
|
headName: agent?.headName ?? 'default', omittedDocuments: agent?.omittedDocuments ?? [],
|
|
11
11
|
ignoredSearchTerms: agent?.ignoredSearchTerms ?? 0 };
|
|
12
12
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { sha256, stableJson } from "../infrastructure/util.js";
|
|
5
|
+
import { assertDocumentTraversalConfig } from './document-traversal.js';
|
|
5
6
|
export function resolveStorageRoot(repositoryRoot, config, userCacheRoot = resolve(homedir(), '.cache', 'context-graph')) {
|
|
6
7
|
if (config.storageScope !== 'user')
|
|
7
8
|
return resolve(repositoryRoot, config.storagePath);
|
|
@@ -14,14 +15,25 @@ const codexOperationalFields = new Set([
|
|
|
14
15
|
'timeoutMs', 'retries', 'concurrency', 'maxDocuments', 'maxSourceBytes',
|
|
15
16
|
]);
|
|
16
17
|
export function semanticCachePartition(repositoryRoot, config) {
|
|
18
|
+
if (config.documentTraversal)
|
|
19
|
+
assertDocumentTraversalConfig(config.documentTraversal);
|
|
17
20
|
const semantic = config.semantic.provider === 'codex-cli'
|
|
18
21
|
? Object.fromEntries(Object.entries(config.semantic).filter(([field]) => !codexOperationalFields.has(field)))
|
|
19
22
|
: config.semantic;
|
|
20
|
-
|
|
23
|
+
// Traversal/role metadata is assembled after the built-in Codex artifact is validated.
|
|
24
|
+
// Keep the old partition exactly when enabling it; do not ignore unknown configuration.
|
|
25
|
+
const { documentTraversal: _traversal, ...directConfig } = config;
|
|
26
|
+
const inputs = config.semantic.provider === 'codex-cli' ? directConfig : config;
|
|
27
|
+
return sha256(stableJson({ config: { ...inputs, semantic }, checkout: resolve(repositoryRoot) }));
|
|
21
28
|
}
|
|
22
29
|
/** Snapshot profiles retain the full configuration. Only Codex user artifacts share operational changes. */
|
|
23
30
|
export function resolveSemanticStorageRoots(repositoryRoot, config, userCacheRoot = resolve(homedir(), '.cache', 'context-graph')) {
|
|
24
31
|
const legacy = resolveStorageRoot(repositoryRoot, config, userCacheRoot);
|
|
32
|
+
// Module providers can inspect arbitrary extraction context. Opt-in traversal isolates their
|
|
33
|
+
// artifacts even in workspace storage, while unchanged committed sources still reuse them.
|
|
34
|
+
if (config.documentTraversal && config.semantic.provider === 'module') {
|
|
35
|
+
return { primary: resolve(legacy, 'document-semantic-v1', semanticCachePartition(repositoryRoot, config)) };
|
|
36
|
+
}
|
|
25
37
|
if (config.storageScope !== 'user' || config.semantic.provider !== 'codex-cli')
|
|
26
38
|
return { primary: legacy };
|
|
27
39
|
return { primary: resolve(userCacheRoot, 'semantic-artifacts-v1', semanticCachePartition(repositoryRoot, config)), legacy };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ScipSourceProject } from './scip-project.js';
|
|
2
|
+
import { type DocumentTraversalConfig } from './document-traversal.js';
|
|
2
3
|
export interface ContextGraphConfig {
|
|
3
4
|
readonly schemaVersion: 1;
|
|
4
5
|
readonly repositoryRoot?: string;
|
|
@@ -6,6 +7,7 @@ export interface ContextGraphConfig {
|
|
|
6
7
|
readonly namespace: string;
|
|
7
8
|
readonly adapter?: string;
|
|
8
9
|
readonly semanticSources: readonly string[];
|
|
10
|
+
readonly documentTraversal?: DocumentTraversalConfig;
|
|
9
11
|
/** Explicit owner-approved source paths/globs; other model-derived prose remains inferred. */
|
|
10
12
|
readonly approvedIntentSources?: readonly string[];
|
|
11
13
|
readonly codeRoots: readonly string[];
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, resolve } from 'node:path';
|
|
5
5
|
import { assertScipSourceProject, assertSourceProjectInvocation } from './scip-project.js';
|
|
6
|
+
import { assertDocumentTraversalConfig } from './document-traversal.js';
|
|
6
7
|
function isRecord(value) {
|
|
7
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
9
|
}
|
|
@@ -34,6 +35,10 @@ function validateConfig(value, absolutePath) {
|
|
|
34
35
|
if (value.approvedIntentSources !== undefined) {
|
|
35
36
|
requireConfig(isStringArray(value.approvedIntentSources), 'approvedIntentSources must be an array of strings', absolutePath);
|
|
36
37
|
}
|
|
38
|
+
if (value.documentTraversal !== undefined) {
|
|
39
|
+
assertDocumentTraversalConfig(value.documentTraversal);
|
|
40
|
+
requireConfig(value.agentContext === undefined, 'documentTraversal and bounded agentContext selection cannot be combined', absolutePath);
|
|
41
|
+
}
|
|
37
42
|
if (value.routingSources !== undefined) {
|
|
38
43
|
requireConfig(isStringArray(value.routingSources) && value.routingSources.length <= 16 &&
|
|
39
44
|
new Set(value.routingSources).size === value.routingSources.length &&
|
|
@@ -113,7 +118,7 @@ function validateConfig(value, absolutePath) {
|
|
|
113
118
|
requireConfig(auth.apiKeyEnv === undefined || (typeof auth.apiKeyEnv === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(auth.apiKeyEnv)), 'semantic.auth.apiKeyEnv must name an environment variable', absolutePath);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
for (const [field, max] of [['timeoutMs',
|
|
121
|
+
for (const [field, max] of [['timeoutMs', 600_000], ['retries', 2], ['maxDocuments', 1000], ['maxSourceBytes', 1_048_576]]) {
|
|
117
122
|
const item = value.semantic[field];
|
|
118
123
|
if (item !== undefined)
|
|
119
124
|
requireConfig(Number.isInteger(item) && Number(item) >= (field === 'retries' ? 0 : 1) && Number(item) <= max, `semantic.${field} exceeds supported bounds`, absolutePath);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DocumentTraversalOptions } from '../extraction/document-traversal.js';
|
|
2
|
+
import { type DocumentClassificationPolicy } from '../extraction/document-classification.js';
|
|
3
|
+
/** Explicit execution mode; the direct semanticSources list is not added to its closure. */
|
|
4
|
+
export interface DocumentTraversalConfig extends Omit<DocumentTraversalOptions, 'maxDocuments' | 'maxSourceBytes'> {
|
|
5
|
+
readonly policy?: DocumentClassificationPolicy;
|
|
6
|
+
}
|
|
7
|
+
export declare function assertDocumentTraversalConfig(value: unknown): asserts value is DocumentTraversalConfig;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { validateDocumentTraversalOptions } from '../extraction/document-traversal.js';
|
|
2
|
+
import { parseDocumentClassificationPolicy } from '../extraction/document-classification.js';
|
|
3
|
+
export function assertDocumentTraversalConfig(value) {
|
|
4
|
+
const fields = ['entryPoints', 'roots', 'maxReadDocuments', 'maxReadBytes', 'maxDocumentBytes', 'policy'];
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
6
|
+
Object.keys(value).some(key => !fields.includes(key))) {
|
|
7
|
+
throw new Error('documentTraversal must contain only supported traversal options and policy');
|
|
8
|
+
}
|
|
9
|
+
const options = value;
|
|
10
|
+
validateDocumentTraversalOptions(options);
|
|
11
|
+
if (options.policy !== undefined)
|
|
12
|
+
parseDocumentClassificationPolicy(options.policy);
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CanonicalDocumentDefinition {
|
|
2
|
+
readonly target: string;
|
|
3
|
+
readonly startLine: number;
|
|
4
|
+
readonly endLine: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CanonicalDocumentLink {
|
|
7
|
+
readonly kind: 'link' | 'ambiguous-reference';
|
|
8
|
+
readonly target?: string;
|
|
9
|
+
readonly startLine: number;
|
|
10
|
+
readonly endLine: number;
|
|
11
|
+
readonly identifier?: string;
|
|
12
|
+
readonly definitions?: readonly CanonicalDocumentDefinition[];
|
|
13
|
+
/** Reference uses and their destination declarations can occur on different lines. */
|
|
14
|
+
readonly referenceStartLine?: number;
|
|
15
|
+
readonly referenceEndLine?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Explicit Markdown links only: this deliberately does not change the graph linker's policy. */
|
|
18
|
+
export declare function canonicalDocumentLinks(content: string): readonly CanonicalDocumentLink[];
|
|
19
|
+
export type CanonicalDocumentLinkResolution = {
|
|
20
|
+
readonly kind: 'path';
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly directoryHint?: true;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'external' | 'fragment' | 'malformed' | 'outside-repository';
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
};
|
|
27
|
+
/** Resolve one explicit destination without consulting the checkout, guessing files or fetching URLs. */
|
|
28
|
+
export declare function resolveCanonicalDocumentLink(sourcePath: string, target: string): CanonicalDocumentLinkResolution;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
3
|
+
/** Explicit Markdown links only: this deliberately does not change the graph linker's policy. */
|
|
4
|
+
export function canonicalDocumentLinks(content) {
|
|
5
|
+
const tree = fromMarkdown(content);
|
|
6
|
+
const definitions = new Map();
|
|
7
|
+
const collect = (node) => {
|
|
8
|
+
if (node.type === 'definition') {
|
|
9
|
+
const existing = definitions.get(node.identifier) ?? [];
|
|
10
|
+
existing.push(node);
|
|
11
|
+
definitions.set(node.identifier, existing);
|
|
12
|
+
}
|
|
13
|
+
if ('children' in node)
|
|
14
|
+
for (const child of node.children)
|
|
15
|
+
collect(child);
|
|
16
|
+
};
|
|
17
|
+
collect(tree);
|
|
18
|
+
const links = [];
|
|
19
|
+
const visit = (node) => {
|
|
20
|
+
if (node.type === 'link' && node.position) {
|
|
21
|
+
links.push({ kind: 'link', target: node.url,
|
|
22
|
+
startLine: node.position.start.line, endLine: node.position.end.line });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (node.type === 'linkReference' && node.position) {
|
|
26
|
+
const declared = (definitions.get(node.identifier) ?? []).flatMap(definition => definition.position
|
|
27
|
+
? [{ target: definition.url, startLine: definition.position.start.line, endLine: definition.position.end.line }]
|
|
28
|
+
: []);
|
|
29
|
+
const first = declared[0];
|
|
30
|
+
if (!first)
|
|
31
|
+
return;
|
|
32
|
+
const reference = { identifier: node.identifier,
|
|
33
|
+
referenceStartLine: node.position.start.line, referenceEndLine: node.position.end.line };
|
|
34
|
+
if (new Set(declared.map(definition => definition.target)).size > 1) {
|
|
35
|
+
// Never silently choose the first conflicting definition, as normal Markdown rendering does.
|
|
36
|
+
links.push({ kind: 'ambiguous-reference', ...reference,
|
|
37
|
+
startLine: node.position.start.line, endLine: node.position.end.line, definitions: declared });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
links.push({ kind: 'link', ...first, ...reference });
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (node.type === 'image' || node.type === 'imageReference' || node.type === 'code' ||
|
|
45
|
+
node.type === 'inlineCode' || node.type === 'html' || node.type === 'definition')
|
|
46
|
+
return;
|
|
47
|
+
if ('children' in node)
|
|
48
|
+
for (const child of node.children)
|
|
49
|
+
visit(child);
|
|
50
|
+
};
|
|
51
|
+
visit(tree);
|
|
52
|
+
return links;
|
|
53
|
+
}
|
|
54
|
+
const unsafeCharacters = /[\\\x00-\x1f\x7f-\x9f]/;
|
|
55
|
+
const scheme = /^[a-z][a-z0-9+.-]*:/i;
|
|
56
|
+
function isCanonicalDocumentSourcePath(path) {
|
|
57
|
+
return path.length > 0 && !unsafeCharacters.test(path) && !posix.isAbsolute(path) &&
|
|
58
|
+
path !== '.' && path !== '..' && !path.startsWith('../') && !path.endsWith('/') &&
|
|
59
|
+
posix.normalize(path) === path;
|
|
60
|
+
}
|
|
61
|
+
/** Resolve one explicit destination without consulting the checkout, guessing files or fetching URLs. */
|
|
62
|
+
export function resolveCanonicalDocumentLink(sourcePath, target) {
|
|
63
|
+
if (!isCanonicalDocumentSourcePath(sourcePath)) {
|
|
64
|
+
return { kind: 'malformed', reason: 'Source must be a canonical repository-relative file path.' };
|
|
65
|
+
}
|
|
66
|
+
if (unsafeCharacters.test(target)) {
|
|
67
|
+
return { kind: 'malformed', reason: 'Backslashes and control characters are unsupported in link destinations.' };
|
|
68
|
+
}
|
|
69
|
+
if (target.startsWith('//') || scheme.test(target)) {
|
|
70
|
+
return { kind: 'external', reason: 'External destinations are reported without fetching them.' };
|
|
71
|
+
}
|
|
72
|
+
const rawPath = target.split(/[?#]/, 1)[0];
|
|
73
|
+
if (!rawPath)
|
|
74
|
+
return { kind: 'fragment', reason: 'Same-document links do not select another file.' };
|
|
75
|
+
let decoded;
|
|
76
|
+
try {
|
|
77
|
+
decoded = decodeURIComponent(rawPath);
|
|
78
|
+
// Plain relative ../ is supported. Encoded traversal segments are intentionally not followed.
|
|
79
|
+
if (rawPath.split('/').some(segment => decodeURIComponent(segment).split('/')
|
|
80
|
+
.some(part => (part === '.' || part === '..') && part !== segment))) {
|
|
81
|
+
return { kind: 'malformed', reason: 'Percent-encoded traversal segments are unsupported.' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { kind: 'malformed', reason: 'Link destination contains malformed percent encoding.' };
|
|
86
|
+
}
|
|
87
|
+
if (unsafeCharacters.test(decoded) || decoded.startsWith('//') || scheme.test(decoded)) {
|
|
88
|
+
return { kind: 'malformed', reason: 'Decoded destination is not a supported repository path.' };
|
|
89
|
+
}
|
|
90
|
+
const path = posix.normalize(decoded.startsWith('/') ? decoded.slice(1) : posix.join(posix.dirname(sourcePath), decoded));
|
|
91
|
+
if (path === '..' || path.startsWith('../') || posix.isAbsolute(path)) {
|
|
92
|
+
return { kind: 'outside-repository', reason: 'Link destination escapes the repository root.' };
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'path', path: path.replace(/\/$/, ''), ...(decoded.endsWith('/') ? { directoryHint: true } : {}) };
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type SourceDocumentRole, type SourceOwnership } from '@weftlet/core';
|
|
2
|
+
export interface DocumentClassificationPolicy {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly roles?: readonly {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly pattern: string;
|
|
7
|
+
readonly role: Exclude<SourceDocumentRole, 'unclassified'>;
|
|
8
|
+
}[];
|
|
9
|
+
readonly owners?: readonly {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly pattern: string;
|
|
12
|
+
readonly ownerDocument: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface DocumentClassificationContext {
|
|
16
|
+
readonly approvedIntentSources: readonly string[];
|
|
17
|
+
readonly evidenceRoots: readonly string[];
|
|
18
|
+
readonly availableDocuments: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export interface DocumentClassification {
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly sourceRole: SourceDocumentRole;
|
|
23
|
+
readonly roleStatus: 'default' | 'explicit' | 'ambiguous';
|
|
24
|
+
readonly matchedRoleMappings: readonly string[];
|
|
25
|
+
readonly approvedBy: readonly string[];
|
|
26
|
+
readonly sourceAuthority: 'declared' | 'inferred';
|
|
27
|
+
readonly ownership: SourceOwnership;
|
|
28
|
+
}
|
|
29
|
+
export interface DocumentClassificationIssue {
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly severity: 'warning' | 'error';
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly paths: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export interface DocumentClassificationReport {
|
|
36
|
+
readonly policyDigest: string;
|
|
37
|
+
readonly documents: readonly DocumentClassification[];
|
|
38
|
+
readonly issues: readonly DocumentClassificationIssue[];
|
|
39
|
+
readonly complete: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Parse bounded explicit policy; normalize ordering, never infer approval or ownership. */
|
|
42
|
+
export declare function parseDocumentClassificationPolicy(value: unknown): DocumentClassificationPolicy;
|
|
43
|
+
/** Classify source-level evidence only; links, owner mappings and roles never approve a child. */
|
|
44
|
+
export declare function classifyDocumentSources(paths: readonly string[], context: DocumentClassificationContext, policy?: DocumentClassificationPolicy): DocumentClassificationReport;
|