@weftlet/toolkit 0.5.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.
Files changed (73) hide show
  1. package/README.md +139 -14
  2. package/dist/application/cache-adoption.d.ts +2 -1
  3. package/dist/application/cache-adoption.js +12 -5
  4. package/dist/application/document-coverage.d.ts +28 -0
  5. package/dist/application/document-coverage.js +58 -0
  6. package/dist/application/document-overlay.d.ts +14 -0
  7. package/dist/application/document-overlay.js +31 -0
  8. package/dist/application/extraction-identity.d.ts +2 -1
  9. package/dist/application/extraction-identity.js +7 -0
  10. package/dist/application/hydration-inputs.d.ts +15 -0
  11. package/dist/application/hydration-inputs.js +55 -0
  12. package/dist/application/hydration-plan.d.ts +7 -0
  13. package/dist/application/hydration-plan.js +18 -5
  14. package/dist/application/hydrator.js +26 -14
  15. package/dist/application/overlay.js +26 -8
  16. package/dist/application/saved-evidence.d.ts +2 -2
  17. package/dist/application/semantic-scope.d.ts +1 -1
  18. package/dist/application/semantic-scope.js +2 -2
  19. package/dist/configuration/cache.js +13 -1
  20. package/dist/configuration/config.d.ts +2 -0
  21. package/dist/configuration/config.js +5 -0
  22. package/dist/configuration/document-traversal.d.ts +7 -0
  23. package/dist/configuration/document-traversal.js +13 -0
  24. package/dist/extraction/canonical-document-links.d.ts +28 -0
  25. package/dist/extraction/canonical-document-links.js +95 -0
  26. package/dist/extraction/document-classification.d.ts +44 -0
  27. package/dist/extraction/document-classification.js +161 -0
  28. package/dist/extraction/document-evidence.d.ts +21 -0
  29. package/dist/extraction/document-evidence.js +83 -0
  30. package/dist/extraction/document-traversal.d.ts +76 -0
  31. package/dist/extraction/document-traversal.js +210 -0
  32. package/dist/extraction/document.d.ts +3 -1
  33. package/dist/extraction/document.js +3 -1
  34. package/dist/graph/authority.js +3 -0
  35. package/dist/graph/coverage.d.ts +8 -0
  36. package/dist/graph/coverage.js +96 -0
  37. package/dist/graph/document-role-description.d.ts +5 -0
  38. package/dist/graph/document-role-description.js +18 -0
  39. package/dist/graph/query.d.ts +2 -2
  40. package/dist/graph/query.js +22 -62
  41. package/dist/graph/rdf.js +11 -0
  42. package/dist/graph/shacl-shapes.js +26 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.js +4 -0
  45. package/dist/interfaces/cli.js +18 -1
  46. package/dist/interfaces/document-coverage.d.ts +9 -0
  47. package/dist/interfaces/document-coverage.js +117 -0
  48. package/dist/repository/git.d.ts +11 -0
  49. package/dist/repository/git.js +38 -0
  50. package/dist/visualization/visualization.js +21 -6
  51. package/docs/ADR-017-shacl-validation-union.md +4 -0
  52. package/docs/ADR-021-identity-preflight-and-validation-timing.md +5 -2
  53. package/docs/ADR-022-release-documentation.md +44 -0
  54. package/docs/ADR-023-document-coverage-preview.md +64 -0
  55. package/docs/ADR-024-implementation-coverage.md +51 -0
  56. package/docs/ADR-025-document-classification.md +78 -0
  57. package/docs/ADR-026-traversal-hydration.md +94 -0
  58. package/docs/ADR-027-validation-closeout.md +155 -0
  59. package/docs/CONSUMER-TRIAL.md +1 -1
  60. package/docs/MIGRATING-TO-WEFTLET.md +1 -1
  61. package/docs/PROVIDERS.md +10 -1
  62. package/docs/RELEASE-1.0-PLAN.md +135 -23
  63. package/docs/RELEASING.md +46 -8
  64. package/docs/ROADMAP.md +41 -16
  65. package/docs/VALIDATION.md +312 -0
  66. package/docs/benchmarks/validation-2026-10-28/README.md +25 -0
  67. package/docs/benchmarks/validation-2026-10-28/large.json +1 -0
  68. package/docs/benchmarks/validation-2026-10-28/lifecycle.json +1 -0
  69. package/docs/benchmarks/validation-2026-10-28/native.json +1 -0
  70. package/docs/benchmarks/validation-2026-10-28/rich.json +1 -0
  71. package/docs/extraction-manifest.json +67 -17
  72. package/examples/generic-typescript/document-policy.json +9 -0
  73. package/package.json +7 -4
@@ -0,0 +1,96 @@
1
+ import { repositoryFileNode } from '@weftlet/core';
2
+ import { contextPackEvidenceAuthority } from './authority.js';
3
+ export const contextPackLanes = [
4
+ 'productIntent',
5
+ 'contractsAndInvariants',
6
+ 'implementationAndConsumers',
7
+ 'validationEvidence',
8
+ ];
9
+ function isValidationFact(fact) {
10
+ if (fact.sourceRole !== undefined) {
11
+ return fact.sourceRole === 'validation' || fact.subject.kind === 'Evidence' ||
12
+ fact.object.kind === 'Evidence' || fact.predicate === 'verifiedBy';
13
+ }
14
+ const path = fact.provenance.path.toLowerCase();
15
+ return (fact.subject.kind === 'Evidence' ||
16
+ fact.object.kind === 'Evidence' ||
17
+ fact.predicate === 'verifiedBy' ||
18
+ /(?:^|\/)(?:test|tests|testing|validation|evidence)(?:\/|$)/.test(path) ||
19
+ /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(path));
20
+ }
21
+ export function isDeclaredFact(fact) {
22
+ if (fact.sourceRole !== undefined && fact.sourceRole !== 'intent')
23
+ return false;
24
+ if (fact.authority)
25
+ return fact.authority === 'declared' && !isValidationFact(fact);
26
+ return false;
27
+ }
28
+ export function contextPackLaneForFact(fact) {
29
+ if (fact.sourceRole === 'validation')
30
+ return 'validationEvidence';
31
+ if (fact.extractor.id === 'otlp-runtime-evidence')
32
+ return undefined;
33
+ const kinds = new Set([fact.subject.kind, fact.object.kind]);
34
+ if (isValidationFact(fact)) {
35
+ return 'validationEvidence';
36
+ }
37
+ if (kinds.has('Contract') ||
38
+ kinds.has('Invariant') ||
39
+ kinds.has('PersistenceModel') ||
40
+ ['governedBy', 'definedBy', 'persistsTo', 'authorizedBy'].includes(fact.predicate)) {
41
+ return 'contractsAndInvariants';
42
+ }
43
+ if ((fact.sourceRole === undefined || fact.sourceRole === 'intent') &&
44
+ (isDeclaredFact(fact) || fact.sourceAuthority === 'declared') &&
45
+ (kinds.has('Document') ||
46
+ kinds.has('Capability') ||
47
+ kinds.has('Workflow') ||
48
+ ['decisionAuthority', 'operationallyOwnedBy', 'references'].includes(fact.predicate))) {
49
+ return 'productIntent';
50
+ }
51
+ return 'implementationAndConsumers';
52
+ }
53
+ export function coverageDescription(coverage) {
54
+ return coverage.scope === 'returned-page' && coverage.countsAreNonExclusive
55
+ ? 'Counts describe evidence on this returned page and may overlap; a code relationship can also describe a contract. They are not a percentage, a count of display groups, or proof that behavior is correct.'
56
+ : 'Coverage scope and overlap metadata are unavailable for this Context Pack; re-query saved evidence for an explicit report.';
57
+ }
58
+ /** Reporting only: never changes a fact's authority, identity, primary lane or stored bytes. */
59
+ export function isImplementationEvidence(fact) {
60
+ if (fact.sourceRole !== undefined || isValidationFact(fact) || fact.extractor.id === 'otlp-runtime-evidence' ||
61
+ fact.extractionReview === 'unreviewed' || contextPackEvidenceAuthority(fact) !== 'implemented')
62
+ return false;
63
+ const path = fact.provenance.path;
64
+ if (!path || /(?:^|\/)(?:test|tests|testing|validation|evidence|__tests__|__mocks__)(?:\/|$)/i.test(path.replaceAll('\\', '/')))
65
+ return false;
66
+ const file = repositoryFileNode(path);
67
+ if (file.kind !== 'Component')
68
+ return false;
69
+ const nodes = [fact.subject, fact.object];
70
+ // Explicit adapter authority also supports logical component/provider identities.
71
+ if (fact.authority === 'implemented')
72
+ return nodes.some(node => ['Component', 'Provider'].includes(node.kind));
73
+ // Legacy deterministic adapters omitted authority. Require their cited physical file,
74
+ // rather than treating every fact in the implementation display group as code evidence.
75
+ return nodes.some(node => node.kind === 'Component' && node.id === file.id);
76
+ }
77
+ export function coverageForFacts(facts) {
78
+ const counts = {
79
+ productIntent: 0, contractsAndInvariants: 0, implementationAndConsumers: 0, validationEvidence: 0,
80
+ };
81
+ for (const fact of facts) {
82
+ const lane = contextPackLaneForFact(fact);
83
+ if (lane && lane !== 'implementationAndConsumers')
84
+ counts[lane]++;
85
+ if (isImplementationEvidence(fact))
86
+ counts.implementationAndConsumers++;
87
+ }
88
+ return { scope: 'returned-page', countsAreNonExclusive: true, counts,
89
+ gaps: [
90
+ ...contextPackLanes.filter(lane => counts[lane] === 0)
91
+ .map(lane => `No ${lane} evidence appears on this returned page.`),
92
+ ...(facts.some(fact => fact.sourceAuthority === 'declared' && fact.extractionReview === 'unreviewed')
93
+ ? ['Approved-source extraction remains unreviewed; verify cited intent before using it as a rule or gate.'] : []),
94
+ ],
95
+ };
96
+ }
@@ -0,0 +1,5 @@
1
+ import type { DocumentSelectionRecord, GraphFact } from '@weftlet/core';
2
+ /** Plain text for evidence details; the rendering context must escape it. */
3
+ export declare function sourceDocumentDescription(fact: GraphFact): string | undefined;
4
+ /** Completeness is scoped to the configured canonical links, not all product knowledge. */
5
+ export declare function documentSelectionDescription(selection?: DocumentSelectionRecord): string;
@@ -0,0 +1,18 @@
1
+ /** Plain text for evidence details; the rendering context must escape it. */
2
+ export function sourceDocumentDescription(fact) {
3
+ const parts = [];
4
+ if (fact.sourceRole !== undefined)
5
+ parts.push(`source role ${fact.sourceRole}`);
6
+ if (fact.sourceOwnership) {
7
+ parts.push(`ownership ${fact.sourceOwnership.status}`, `owner documents ${fact.sourceOwnership.ownerDocuments.join(', ') || '(none)'}`);
8
+ }
9
+ return parts.length ? parts.join('; ') : undefined;
10
+ }
11
+ /** Completeness is scoped to the configured canonical links, not all product knowledge. */
12
+ export function documentSelectionDescription(selection) {
13
+ if (!selection)
14
+ return '';
15
+ return `Document traversal: ${selection.complete ? 'complete within configured links' : 'INCOMPLETE'}; ` +
16
+ `${selection.documentPaths.length} discovered documents; ${selection.semanticPaths.length} semantic candidates; ` +
17
+ `${selection.excluded.length} excluded from semantic extraction. Ownership and assertion approval are separate.`;
18
+ }
@@ -1,7 +1,7 @@
1
- import type { ContextPack, ContextPackLane, ContextPackRenderer, GraphFact, ImpactQueryService, ImpactScanRequest, SnapshotStore } from "@weftlet/core";
1
+ import type { ContextPack, ContextPackRenderer, ImpactQueryService, ImpactScanRequest, SnapshotStore } from "@weftlet/core";
2
+ export { contextPackLaneForFact } from './coverage.js';
2
3
  export declare const DEFAULT_CONTEXT_PACK_FACTS = 60;
3
4
  export declare const MAX_CONTEXT_PACK_FACTS = 200;
4
- export declare function contextPackLaneForFact(fact: GraphFact): ContextPackLane | undefined;
5
5
  export { contextPackEvidenceAuthority } from "./authority.js";
6
6
  export declare class MarkdownContextPackRenderer implements ContextPackRenderer {
7
7
  render(pack: Omit<ContextPack, 'markdown'>): string;
@@ -4,49 +4,11 @@ import { RdfEvidenceGraph } from "./rdf.js";
4
4
  import { sha256, stableJson } from "../infrastructure/util.js";
5
5
  import { diagnosticSummary, selectDiagnostics } from "./diagnostics.js";
6
6
  import { factSearchText, IMPACT_RANKING_VERSION, rankImpactFacts } from './ranking.js';
7
+ import { contextPackLanes, contextPackLaneForFact, isDeclaredFact, coverageForFacts, coverageDescription } from './coverage.js';
8
+ import { documentSelectionDescription, sourceDocumentDescription } from './document-role-description.js';
9
+ export { contextPackLaneForFact } from './coverage.js';
7
10
  export const DEFAULT_CONTEXT_PACK_FACTS = 60;
8
11
  export const MAX_CONTEXT_PACK_FACTS = 200;
9
- const contextPackLanes = [
10
- 'productIntent',
11
- 'contractsAndInvariants',
12
- 'implementationAndConsumers',
13
- 'validationEvidence',
14
- ];
15
- function isValidationFact(fact) {
16
- const path = fact.provenance.path.toLowerCase();
17
- return (fact.subject.kind === 'Evidence' ||
18
- fact.object.kind === 'Evidence' ||
19
- fact.predicate === 'verifiedBy' ||
20
- /(?:^|\/)(?:test|tests|testing|validation|evidence)(?:\/|$)/.test(path) ||
21
- /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(path));
22
- }
23
- function isDeclaredFact(fact) {
24
- if (fact.authority)
25
- return fact.authority === 'declared' && !isValidationFact(fact);
26
- return false;
27
- }
28
- export function contextPackLaneForFact(fact) {
29
- if (fact.extractor.id === 'otlp-runtime-evidence')
30
- return undefined;
31
- const kinds = new Set([fact.subject.kind, fact.object.kind]);
32
- if (isValidationFact(fact)) {
33
- return 'validationEvidence';
34
- }
35
- if (kinds.has('Contract') ||
36
- kinds.has('Invariant') ||
37
- kinds.has('PersistenceModel') ||
38
- ['governedBy', 'definedBy', 'persistsTo', 'authorizedBy'].includes(fact.predicate)) {
39
- return 'contractsAndInvariants';
40
- }
41
- if ((isDeclaredFact(fact) || fact.sourceAuthority === 'declared') &&
42
- (kinds.has('Document') ||
43
- kinds.has('Capability') ||
44
- kinds.has('Workflow') ||
45
- ['decisionAuthority', 'operationallyOwnedBy', 'references'].includes(fact.predicate))) {
46
- return 'productIntent';
47
- }
48
- return 'implementationAndConsumers';
49
- }
50
12
  export { contextPackEvidenceAuthority } from "./authority.js";
51
13
  function selectBalancedFacts(ranked, maxFacts) {
52
14
  const queues = Object.fromEntries(contextPackLanes.map((lane) => [
@@ -63,11 +25,7 @@ function selectBalancedFacts(ranked, maxFacts) {
63
25
  break;
64
26
  }
65
27
  }
66
- const counts = Object.fromEntries(contextPackLanes.map((lane) => [
67
- lane,
68
- facts.filter((fact) => contextPackLaneForFact(fact) === lane).length,
69
- ]));
70
- return { facts, counts };
28
+ return facts;
71
29
  }
72
30
  const searchStopWords = new Set([
73
31
  'about',
@@ -99,6 +57,12 @@ function boundedFactLimit(value) {
99
57
  }
100
58
  return Math.min(value, MAX_CONTEXT_PACK_FACTS);
101
59
  }
60
+ function markdownFactDetails(fact) {
61
+ const document = sourceDocumentDescription(fact)?.replace(/[\\`*_{}\[\]()<>!|]/g, '\\$&').replace(/[\r\n]/g, ' ');
62
+ return `confidence ${fact.confidence}` +
63
+ (fact.sourceAuthority ? `; source ${fact.sourceAuthority}; extraction ${fact.extractionReview ?? 'unknown'}` : '') +
64
+ (document ? `; ${document}` : '');
65
+ }
102
66
  export class MarkdownContextPackRenderer {
103
67
  render(pack) {
104
68
  const lines = [
@@ -106,6 +70,7 @@ export class MarkdownContextPackRenderer {
106
70
  '',
107
71
  `Snapshot: \`${pack.snapshot.snapshotId}\` (${pack.snapshot.commitSha})`,
108
72
  `Configuration: ${pack.snapshot.configurationDigest ?? 'legacy/unknown'}; scope: ${pack.snapshot.scope?.join(', ') ?? 'legacy/unknown'}`,
73
+ ...[documentSelectionDescription(pack.snapshot.documentSelection)].filter(Boolean),
109
74
  ...(pack.exploration ? [
110
75
  `Ranking: ${pack.exploration.rankingVersion ?? 'legacy/unknown'}`,
111
76
  `Relationships returned: ${pack.exploration.returned}/${pack.exploration.reachableFacts}; truncated: ${pack.exploration.truncated}; depth limit reached: ${pack.exploration.depthLimitReached}`,
@@ -130,17 +95,18 @@ export class MarkdownContextPackRenderer {
130
95
  ...pack.invariants.map((item) => `- ${item.status}: ${item.message}`),
131
96
  '',
132
97
  '## Coverage',
98
+ coverageDescription(pack.coverage),
133
99
  ...contextPackLanes.map((lane) => `- ${lane}: ${pack.coverage.counts[lane]} relationships`),
134
100
  ...(pack.coverage.gaps.length
135
101
  ? pack.coverage.gaps.map((gap) => `- GAP: ${gap}`)
136
- : ['- No Context Pack lane gaps detected.']),
102
+ : ['- No coverage gaps detected on this returned page.']),
137
103
  ...contextPackLanes.flatMap((lane) => [
138
104
  '',
139
105
  `## ${lane}`,
140
106
  ...pack.facts
141
107
  .filter((fact) => contextPackLaneForFact(fact) === lane)
142
108
  .map((fact) => `- [${contextPackEvidenceAuthority(fact)}] ${fact.subject.label} —${fact.predicate}→ ${fact.object.label} ` +
143
- `(\`${fact.provenance.path}:${fact.provenance.startLine}\`, confidence ${fact.confidence}${fact.sourceAuthority ? `; source ${fact.sourceAuthority}; extraction ${fact.extractionReview ?? 'unknown'}` : ''})`),
109
+ `(\`${fact.provenance.path}:${fact.provenance.startLine}\`, ${markdownFactDetails(fact)})`),
144
110
  ]),
145
111
  '',
146
112
  '## Runtime evidence',
@@ -226,7 +192,7 @@ export class SnapshotImpactQueryService {
226
192
  }
227
193
  // Balance evidence lanes among similarly ranked facts, not across unrelated distance tiers.
228
194
  const allBalanced = [...tiers.values()].flatMap((tier) => [
229
- ...selectBalancedFacts(tier, tier.length).facts,
195
+ ...selectBalancedFacts(tier, tier.length),
230
196
  ...tier.filter((fact) => contextPackLaneForFact(fact) === undefined),
231
197
  ]);
232
198
  const { continuation: _continuation, snapshotId: _snapshotId, ...query } = request;
@@ -249,7 +215,7 @@ export class SnapshotImpactQueryService {
249
215
  offset = token.offset;
250
216
  }
251
217
  const facts = allBalanced.slice(offset, offset + maxFacts);
252
- const balanced = selectBalancedFacts(facts, facts.length);
218
+ const coverage = coverageForFacts(facts);
253
219
  const nextOffset = offset + facts.length;
254
220
  const continuation = nextOffset < allBalanced.length ? Buffer.from(JSON.stringify({
255
221
  snapshot: snapshot.manifest.snapshotId, digest: snapshot.manifest.contentDigest,
@@ -294,13 +260,13 @@ export class SnapshotImpactQueryService {
294
260
  for (const [kind, missing, reason] of [
295
261
  [
296
262
  'missingIntent',
297
- balanced.counts.productIntent === 0,
298
- 'No declared intent matched this query. Inspect the owner and philosophy sources before concluding a requirement is missing.',
263
+ coverage.counts.productIntent === 0,
264
+ 'No product-intent relationships appear on this returned page. Inspect continuation, scope and owner sources before concluding a requirement is missing.',
299
265
  ],
300
266
  [
301
267
  'implementationGaps',
302
- balanced.counts.implementationAndConsumers === 0,
303
- 'No implementation relationships matched this query. Inspect code before concluding behavior is missing.',
268
+ coverage.counts.implementationAndConsumers === 0,
269
+ 'No production implementation evidence appears on this returned page. Inspect continuation, scope and code before concluding behavior is missing.',
304
270
  ],
305
271
  [
306
272
  'runtimeGaps',
@@ -329,20 +295,14 @@ export class SnapshotImpactQueryService {
329
295
  overlayDigest: snapshot.manifest.overlayDigest,
330
296
  overlayCapture: snapshot.manifest.overlayCapture,
331
297
  extractorVersions: snapshot.manifest.extractorVersions,
298
+ ...(snapshot.manifest.documentSelection ? { documentSelection: snapshot.manifest.documentSelection } : {}),
332
299
  },
333
300
  capabilities,
334
301
  facts,
335
302
  invariants: snapshot.invariants.filter((item) => item.provenance.some((span) => suggestedFiles.includes(span.path))),
336
303
  runtimeEvidence,
337
304
  suggestedFiles,
338
- coverage: {
339
- counts: balanced.counts,
340
- gaps: [...contextPackLanes
341
- .filter((lane) => balanced.counts[lane] === 0)
342
- .map((lane) => `No ${lane} evidence matched this request.`),
343
- ...(facts.some((fact) => fact.sourceAuthority === 'declared' && fact.extractionReview === 'unreviewed')
344
- ? ['Approved-source extraction remains unreviewed; verify cited intent before using it as a rule or gate.'] : [])],
345
- },
305
+ coverage,
346
306
  diagnostics: selectDiagnostics(snapshot, request, facts, focus, queryDiagnostics),
347
307
  exploration: {
348
308
  rankingVersion: IMPACT_RANKING_VERSION,
package/dist/graph/rdf.js CHANGED
@@ -36,8 +36,19 @@ export class RdfEvidenceGraph {
36
36
  snapshotId: fact.snapshotId, record: stableJson(fact),
37
37
  ...(fact.sourceAuthority ? { sourceAuthority: fact.sourceAuthority } : {}),
38
38
  ...(fact.extractionReview ? { extractionReview: fact.extractionReview } : {}),
39
+ ...(fact.sourceRole !== undefined ? { sourceRole: fact.sourceRole } : {}),
40
+ ...(fact.sourcePolicyDigest !== undefined ? { sourcePolicyDigest: fact.sourcePolicyDigest } : {}),
39
41
  }))
40
42
  this.dataset.addQuad(record, iri(CG + p), literal(value));
43
+ if (fact.sourceOwnership !== undefined) {
44
+ this.dataset.addQuad(record, iri(CG + 'ownershipStatus'), literal(fact.sourceOwnership.status));
45
+ for (const ownerDocument of fact.sourceOwnership.ownerDocuments) {
46
+ this.dataset.addQuad(record, iri(CG + 'ownerDocument'), literal(ownerDocument));
47
+ }
48
+ for (const mappingId of fact.sourceOwnership.mappingIds) {
49
+ this.dataset.addQuad(record, iri(CG + 'ownershipMapping'), literal(mappingId));
50
+ }
51
+ }
41
52
  for (const [p, value] of [['startLine', fact.provenance.startLine], ['endLine', fact.provenance.endLine]]) {
42
53
  this.dataset.addQuad(record, iri(CG + p), literal(String(value), iri(XSD + 'integer')));
43
54
  }
@@ -24,4 +24,30 @@ cg:FactShape a sh:NodeShape; sh:targetClass rdf:Statement;
24
24
  sh:property [ sh:path cg:extractor; sh:minCount 1; sh:datatype xsd:string; sh:minLength 1 ].
25
25
  cg:NodeShape a sh:NodeShape; sh:targetClass cg:Node;
26
26
  sh:property [ sh:path cg:kind; sh:minCount 1; sh:maxCount 1 ].
27
+
28
+ # Optional source metadata targets only assertions that carry those predicates.
29
+ # Legacy facts do not acquire additional property or ownership consistency checks.
30
+ cg:SourceRoleShape a sh:NodeShape; sh:targetSubjectsOf cg:sourceRole;
31
+ sh:property [ sh:path cg:sourceRole; sh:maxCount 1; sh:datatype xsd:string;
32
+ sh:in ("intent" "plan" "validation" "reference" "unclassified") ].
33
+ cg:SourcePolicyShape a sh:NodeShape; sh:targetSubjectsOf cg:sourcePolicyDigest;
34
+ sh:property [ sh:path cg:sourcePolicyDigest; sh:maxCount 1; sh:datatype xsd:string; sh:minLength 1 ].
35
+ cg:SourceOwnershipShape a sh:NodeShape;
36
+ sh:targetSubjectsOf cg:ownershipStatus, cg:ownerDocument, cg:ownershipMapping;
37
+ sh:property [ sh:path cg:ownershipStatus; sh:minCount 1; sh:maxCount 1; sh:datatype xsd:string;
38
+ sh:in ("resolved" "missing" "ambiguous" "unavailable" "unapproved" "ineligible" "not-required") ];
39
+ sh:property [ sh:path cg:ownerDocument; sh:datatype xsd:string; sh:minLength 1 ];
40
+ sh:property [ sh:path cg:ownershipMapping; sh:datatype xsd:string; sh:minLength 1 ];
41
+ # Structural consistency does not prove approval, ownership correctness or source availability.
42
+ sh:or (
43
+ [ sh:property [ sh:path cg:ownershipStatus; sh:in ("resolved" "unavailable" "unapproved" "ineligible") ];
44
+ sh:property [ sh:path cg:ownerDocument; sh:minCount 1; sh:maxCount 1 ];
45
+ sh:property [ sh:path cg:ownershipMapping; sh:minCount 1 ] ]
46
+ [ sh:property [ sh:path cg:ownershipStatus; sh:hasValue "ambiguous" ];
47
+ sh:property [ sh:path cg:ownerDocument; sh:minCount 2 ];
48
+ sh:property [ sh:path cg:ownershipMapping; sh:minCount 1 ] ]
49
+ [ sh:property [ sh:path cg:ownershipStatus; sh:in ("missing" "not-required") ];
50
+ sh:property [ sh:path cg:ownerDocument; sh:maxCount 0 ];
51
+ sh:property [ sh:path cg:ownershipMapping; sh:maxCount 0 ] ]
52
+ ).
27
53
  `);
package/dist/index.d.ts CHANGED
@@ -15,3 +15,8 @@ export * from './application/hydration-failure.js';
15
15
  export * from './extraction/semantic-diagnostics.js';
16
16
  export * from './graph/identity-preflight.js';
17
17
  export * from './graph/validation-progress.js';
18
+ export * from './application/document-coverage.js';
19
+ export * from './extraction/document-traversal.js';
20
+ export * from './extraction/document-classification.js';
21
+ export * from './extraction/document-evidence.js';
22
+ export type { DocumentTraversalConfig } from './configuration/document-traversal.js';
package/dist/index.js CHANGED
@@ -16,3 +16,7 @@ export * from './application/hydration-failure.js';
16
16
  export * from './extraction/semantic-diagnostics.js';
17
17
  export * from './graph/identity-preflight.js';
18
18
  export * from './graph/validation-progress.js';
19
+ export * from './application/document-coverage.js';
20
+ export * from './extraction/document-traversal.js';
21
+ export * from './extraction/document-classification.js';
22
+ export * from './extraction/document-evidence.js';
@@ -19,6 +19,7 @@ import { planHydration } from '../application/hydration-plan.js';
19
19
  import { adoptSemanticCache } from '../application/cache-adoption.js';
20
20
  import { SemanticHydrationError } from '../application/hydration-failure.js';
21
21
  import { GraphIdentityConflictError } from '../graph/identity-preflight.js';
22
+ import { runDocumentCoverage } from './document-coverage.js';
22
23
  function valueAfter(args, name) {
23
24
  const index = args.indexOf(name);
24
25
  return index >= 0 ? args[index + 1] : undefined;
@@ -61,9 +62,23 @@ function formatHydrationPlan(plan) {
61
62
  `Expected cache reuse: ${plan.estimates.reusableArtifacts}; extractions: ${plan.estimates.extractions}; unknown cache entries: ${plan.estimates.unknownArtifacts}`,
62
63
  `Source/cache/budget checks: ${readiness}`,
63
64
  ];
65
+ if (plan.documentSelection) {
66
+ const selection = plan.documentSelection;
67
+ lines.push(`Document traversal: ${selection.complete ? 'complete' : 'blocked'}; identity ${selection.digest}; policy ${selection.policyDigest}`);
68
+ lines.push(`Discovered: ${selection.documentPaths.length}; semantic candidates: ${selection.semanticPaths.length}`);
69
+ for (const excluded of selection.excluded)
70
+ lines.push(`Excluded from model extraction: ${JSON.stringify(excluded.path)} (${excluded.reason}); retained traversal/evidence reporting.`);
71
+ }
72
+ if (plan.documentCoverage) {
73
+ const { limits, counters } = plan.documentCoverage.traversal;
74
+ lines.push(`Traversal reads: ${counters.readDocuments}/${limits.maxReadDocuments} documents; ${counters.readBytes}/${limits.maxReadBytes} bytes; ${limits.maxDocumentBytes} bytes maximum per document`);
75
+ }
64
76
  for (const document of plan.documents) {
65
77
  lines.push(`- ${JSON.stringify(document.path)} [${document.sourceId}]: ${document.bytes} bytes, ${document.lines} lines; ${document.role}; source ${document.sourceAuthority}, extraction ${document.extractionReview}; cache ${document.cache.status}; selected by ${document.selectedBy.map((selector) => JSON.stringify(selector)).join(', ')}`);
66
78
  }
79
+ for (const document of plan.documentCoverage?.classification.documents ?? []) {
80
+ lines.push(`Classification ${JSON.stringify(document.path)}: ${document.sourceRole}; owner ${document.ownership.status}; ${document.ownership.ownerDocuments.map(path => JSON.stringify(path)).join(', ')}`);
81
+ }
67
82
  for (const selection of plan.selections.filter((item) => item.status !== 'matched')) {
68
83
  lines.push(`Selection ${JSON.stringify(selection.selector)}: ${selection.status}`);
69
84
  }
@@ -83,9 +98,11 @@ function formatHydrationPlan(plan) {
83
98
  async function main() {
84
99
  const args = process.argv.slice(2);
85
100
  if (args.includes('--help') || !args.length) {
86
- process.stdout.write('weftlet init|config show|config validate|doctor|hydrate|cache adopt|overlay|impact scan|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Preview readiness is advisory and does not add a hydration gate.\nhydrate --json --quiet emits structured semantic failures on stderr with no progress lines; failure stdout stays empty. hydrate --plan maps opaque source IDs to local document paths.\nhydrate|overlay --validation-details explicitly includes local node/fact identities, extractor names and source citations in identity-conflict errors; default reports use opaque references. This flag writes no diagnostic file. Use --json --quiet for structured identity-conflict stderr and empty failure stdout. Validation timings report phase boundaries, not a live heartbeat or percentage.\ncache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run] returns JSON and copies only validated compatible artifacts, without model calls or snapshot writes. --dry-run performs no cache writes. Agent configurations accept the same --owner-doc, --task and --capability selection as hydrate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
101
+ process.stdout.write('weftlet documents plan|init|config show|config validate|doctor|hydrate|cache adopt|overlay|impact scan|sparql|validate|govern|visualize|diagnostics show|invariant check|clean-compare|ontology compile\nUse --config <file>; queries accept --snapshot <id>. impact scan accepts --changed-file, --capability, --max-facts, --max-depth, --continuation.\ndocuments plan --entry <document> [--entry <document>] [--root <path>] [--document-policy <json-file>] [--ref <commit>] [--json] previews prospective committed link coverage; it does not change hydration inputs or estimate model/cache work. Source classification/ownership completeness is separate from traversal readiness. The preview policy is an operation input. Set documentTraversal in configuration to activate traversal hydration; hydrate --plan describes its execution inputs. Safety limits: --max-read-documents, --max-read-bytes, --max-document-bytes.\nhydrate --plan [--json] [--ref <commit>] [--force-semantic] previews scope, cache and budgets without model calls or graph writes. Traversal coverage and role ambiguity block new model work; direct-source selection retains its existing behavior.\nhydrate --json --quiet emits structured semantic failures on stderr with no progress lines; failure stdout stays empty. hydrate --plan maps opaque source IDs to local document paths.\nhydrate|overlay --validation-details explicitly includes local node/fact identities, extractor names and source citations in identity-conflict errors; default reports use opaque references. This flag writes no diagnostic file. Use --json --quiet for structured identity-conflict stderr and empty failure stdout. Validation timings report phase boundaries, not a live heartbeat or percentage.\ncache adopt --from-config <previous.json> [--config <current.json>] [--ref <commit>] [--dry-run] returns JSON and copies only validated compatible artifacts, without model calls or snapshot writes. --dry-run performs no cache writes. Agent configurations accept the same --owner-doc, --task and --capability selection as hydrate.\ninit [--yes] [--directory <project>] [--config <file>] [--repository-id <id>] [--code-root <path>] [--semantic-source <path>] [--evidence-root <path>] [--model <name>] [--no-gitignore]\nSource flags may be repeated. init never overwrites an existing config. See README.md for examples.\n');
87
102
  return;
88
103
  }
104
+ if (await runDocumentCoverage(args, defaultConfigPath))
105
+ return;
89
106
  if (args[0] === 'init') {
90
107
  await runInit(args.slice(1));
91
108
  return;
@@ -0,0 +1,9 @@
1
+ import { type DocumentCoverageOptions, type DocumentCoveragePlan } from '../application/document-coverage.js';
2
+ export declare function parseDocumentCoverageArgs(args: readonly string[]): {
3
+ configPath?: string;
4
+ policyPath?: string;
5
+ json: boolean;
6
+ options: DocumentCoverageOptions;
7
+ } | undefined;
8
+ export declare function formatDocumentCoverage(plan: DocumentCoveragePlan): string;
9
+ export declare function runDocumentCoverage(args: readonly string[], defaultConfig: () => string): Promise<boolean>;
@@ -0,0 +1,117 @@
1
+ import { planDocumentCoverage } from '../application/document-coverage.js';
2
+ import { open } from 'node:fs/promises';
3
+ import { parseDocumentClassificationPolicy } from '../extraction/document-classification.js';
4
+ const traversalFlags = ['--entry', '--root', '--document-policy', '--max-read-documents', '--max-read-bytes', '--max-document-bytes'];
5
+ const usage = 'Usage: weftlet documents plan --entry <document> [--entry <document>] [--root <path>] [--document-policy <json-file>] [--config <file>] [--ref <commit>] [--json] [--max-read-documents <count>] [--max-read-bytes <bytes>] [--max-document-bytes <bytes>]';
6
+ export function parseDocumentCoverageArgs(args) {
7
+ if (args[0] !== 'documents') {
8
+ if (args.some(arg => traversalFlags.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
9
+ throw new Error('Document traversal options are supported only by documents plan; they do not change hydration selection.');
10
+ }
11
+ return undefined;
12
+ }
13
+ if (args[1] !== 'plan')
14
+ throw new Error(usage);
15
+ const values = new Map();
16
+ let json = false;
17
+ for (let index = 2; index < args.length; index++) {
18
+ const flag = args[index];
19
+ if (flag === '--json') {
20
+ if (json)
21
+ throw new Error(usage);
22
+ json = true;
23
+ continue;
24
+ }
25
+ if (![...traversalFlags, '--config', '--ref'].includes(flag))
26
+ throw new Error(usage);
27
+ const value = args[++index];
28
+ if (!value || value.startsWith('--'))
29
+ throw new Error(usage);
30
+ if (values.has(flag) && flag !== '--entry' && flag !== '--root')
31
+ throw new Error(usage);
32
+ values.set(flag, [...(values.get(flag) ?? []), value]);
33
+ }
34
+ if (!values.has('--entry'))
35
+ throw new Error(usage);
36
+ const number = (flag) => {
37
+ const value = values.get(flag)?.[0];
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
41
+ throw new Error(`${flag} must be a positive integer`);
42
+ return Number(value);
43
+ };
44
+ return { configPath: values.get('--config')?.[0], policyPath: values.get('--document-policy')?.[0], json, options: {
45
+ entryPoints: values.get('--entry'), roots: values.get('--root'), ref: values.get('--ref')?.[0],
46
+ maxReadDocuments: number('--max-read-documents'), maxReadBytes: number('--max-read-bytes'),
47
+ maxDocumentBytes: number('--max-document-bytes'),
48
+ } };
49
+ }
50
+ export function formatDocumentCoverage(plan) {
51
+ const lines = [
52
+ `Prospective document coverage: ${plan.repositoryId}`,
53
+ `Commit: ${plan.commitSha} (requested ${JSON.stringify(plan.ref)})`,
54
+ `Configuration: ${plan.configurationDigest}; coverage: ${plan.coverageDigest}`,
55
+ 'Hydration selection unchanged; no extraction/cache estimate.',
56
+ `Entry points: ${plan.entryPoints.map(path => JSON.stringify(path)).join(', ')}`,
57
+ `Roots: ${plan.roots.map(path => JSON.stringify(path)).join(', ')}`,
58
+ `Coverage: ${plan.coverage.complete ? 'complete within supported local scope' : 'incomplete'}; budget comparison: ${plan.budgets.withinLimits ? 'within limits' : 'blocked'}`,
59
+ `Source classification: ${plan.classification.complete ? 'complete' : 'gaps'}; policy: ${plan.classification.policyDigest}. This is separate from traversal/budget readiness.`,
60
+ `Discovered documents: ${plan.budgets.documents}/${plan.budgets.maxDocuments}; bytes: ${plan.budgets.bytes}; maximum per extraction source: ${plan.budgets.maxSourceBytes}`,
61
+ `Read limits: ${plan.limits.maxReadDocuments} documents, ${plan.limits.maxReadBytes} total bytes, ${plan.limits.maxDocumentBytes} bytes per document`,
62
+ ];
63
+ for (const document of plan.documents)
64
+ lines.push(`- ${JSON.stringify(document.path)}: ${document.status}, ${document.bytes} bytes; ${document.role}; role ${document.sourceRole} (${document.roleStatus}); source ${document.sourceAuthority}, extraction ${document.extractionReview}; owner ${document.ownership.status} ${document.ownership.ownerDocuments.map(path => JSON.stringify(path)).join(', ')}; ${document.entryPoint ? 'entry point' : 'linked document'}`);
65
+ for (const link of plan.links)
66
+ lines.push(`Link ${JSON.stringify(link.sourcePath)}:${link.startLine}-${link.endLine}: ${link.status}${link.path ? ` ${JSON.stringify(link.path)}` : ''}`);
67
+ for (const issue of plan.issues)
68
+ lines.push(`${issue.severity.toUpperCase()} [${issue.code}]: ${issue.message}`);
69
+ for (const issue of plan.classification.issues)
70
+ lines.push(`CLASSIFICATION ${issue.severity.toUpperCase()} [${issue.code}]: ${issue.message} ${issue.paths.map(path => JSON.stringify(path)).join(', ')}`);
71
+ if (plan.coverage.unvisited.length)
72
+ lines.push(`Unread frontier: ${plan.coverage.unvisited.map(path => JSON.stringify(path)).join(', ')}`);
73
+ if (plan.excludedWorkingChanges.length)
74
+ lines.push(`Working changes excluded: ${plan.excludedWorkingChanges.map(path => JSON.stringify(path)).join(', ')}`);
75
+ lines.push(...plan.assumptions.map(note => `Note: ${note}`));
76
+ return `${lines.join('\n')}\n`;
77
+ }
78
+ async function readDocumentPolicy(path) {
79
+ const handle = await open(path, 'r');
80
+ try {
81
+ const metadata = await handle.stat();
82
+ if (!metadata.isFile() || metadata.size > 65_536)
83
+ throw new Error('Document policy must be a JSON file of at most 65536 bytes');
84
+ const buffer = Buffer.alloc(65_537);
85
+ let length = 0;
86
+ while (length < buffer.length) {
87
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
88
+ if (!bytesRead)
89
+ break;
90
+ length += bytesRead;
91
+ }
92
+ if (length > 65_536)
93
+ throw new Error('Document policy exceeds 65536 bytes');
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(buffer.subarray(0, length).toString('utf8'));
97
+ }
98
+ catch {
99
+ throw new Error('Document policy must contain valid JSON');
100
+ }
101
+ return parseDocumentClassificationPolicy(parsed);
102
+ }
103
+ finally {
104
+ await handle.close();
105
+ }
106
+ }
107
+ export async function runDocumentCoverage(args, defaultConfig) {
108
+ const parsed = parseDocumentCoverageArgs(args);
109
+ if (!parsed)
110
+ return false;
111
+ const options = parsed.policyPath ? { ...parsed.options, documentPolicy: await readDocumentPolicy(parsed.policyPath) } : parsed.options;
112
+ const plan = await planDocumentCoverage(parsed.configPath ?? defaultConfig(), options);
113
+ process.stdout.write(parsed.json ? `${JSON.stringify(plan, null, 2)}\n` : formatDocumentCoverage(plan));
114
+ if (!plan.ready)
115
+ process.exitCode = 1;
116
+ return true;
117
+ }
@@ -8,6 +8,15 @@ export interface GitTextMatch {
8
8
  readonly path: string;
9
9
  readonly line: number;
10
10
  }
11
+ /** Exact committed entries, relative to the configured repository root. */
12
+ export interface GitTreeEntry {
13
+ readonly path: string;
14
+ readonly mode: string;
15
+ readonly type: 'blob' | 'tree' | 'commit';
16
+ readonly objectId: string;
17
+ /** Git blob size in bytes; directories and gitlinks have no size. */
18
+ readonly size?: number;
19
+ }
11
20
  export declare class GitRepository {
12
21
  readonly root: string;
13
22
  constructor(root: string);
@@ -19,6 +28,8 @@ export declare class GitRepository {
19
28
  requireExactCleanCommit(commitSha: string): Promise<void>;
20
29
  listFilesAt(commitSha: string, roots: readonly string[]): Promise<readonly string[]>;
21
30
  listAllFilesAt(commitSha: string): Promise<readonly string[]>;
31
+ /** Read tree metadata without Git's quoted/newline path formatting or checkout access. */
32
+ listEntriesAt(commitSha: string): Promise<readonly GitTreeEntry[]>;
22
33
  readFileAt(commitSha: string, path: string): Promise<string>;
23
34
  findTextAt(commitSha: string, needles: readonly string[], roots: readonly string[]): Promise<readonly GitTextMatch[]>;
24
35
  delta(baseSha: string | undefined, headSha: string, roots: readonly string[]): Promise<GitDelta>;
@@ -2,6 +2,7 @@
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import { posix } from 'node:path';
5
+ import { isUtf8 } from 'node:buffer';
5
6
  import { sha256, slug } from "../infrastructure/util.js";
6
7
  const execFileAsync = promisify(execFile);
7
8
  export class GitRepository {
@@ -73,6 +74,43 @@ export class GitRepository {
73
74
  listAllFilesAt(commitSha) {
74
75
  return this.listFilesAt(commitSha, ['.']);
75
76
  }
77
+ /** Read tree metadata without Git's quoted/newline path formatting or checkout access. */
78
+ async listEntriesAt(commitSha) {
79
+ const commit = await this.resolveRef(commitSha);
80
+ // :./ selects the committed subtree at cwd; --full-tree makes output relative to
81
+ // that subtree rather than applying cwd's prefix a second time in a nested project.
82
+ const { stdout } = await execFileAsync('git', ['ls-tree', '-r', '-t', '-l', '-z', '--full-tree', `${commit}:./`], {
83
+ cwd: this.root, encoding: 'buffer', maxBuffer: 64 * 1024 * 1024, windowsHide: true,
84
+ });
85
+ if (!isUtf8(stdout))
86
+ throw new Error('Git tree entry paths are not valid UTF-8');
87
+ const output = stdout.toString('utf8');
88
+ if (!output)
89
+ return [];
90
+ if (!output.endsWith('\0'))
91
+ throw new Error('Malformed NUL-delimited Git tree listing');
92
+ const seen = new Set();
93
+ const entries = output.slice(0, -1).split('\0').map((record) => {
94
+ const match = /^([0-7]{6}) (blob|tree|commit) ([a-f0-9]{40}|[a-f0-9]{64}) +([0-9]+|-)\t([\s\S]+)$/.exec(record);
95
+ if (!match)
96
+ throw new Error('Malformed Git tree entry metadata');
97
+ const mode = match[1];
98
+ const type = match[2];
99
+ const objectId = match[3];
100
+ const rawSize = match[4];
101
+ const path = match[5];
102
+ const size = rawSize === '-' ? undefined : Number(rawSize);
103
+ const validMode = type === 'blob' ? ['100644', '100755', '120000'].includes(mode)
104
+ : mode === (type === 'tree' ? '040000' : '160000');
105
+ if (!validMode || (type === 'blob' ? size === undefined || !Number.isSafeInteger(size) || size < 0 : size !== undefined) ||
106
+ path.startsWith('/') || path.split('/').some(part => !part || part === '.' || part === '..') || seen.has(path)) {
107
+ throw new Error('Malformed Git tree entry mode, size or path');
108
+ }
109
+ seen.add(path);
110
+ return { path, mode, type, objectId, ...(size === undefined ? {} : { size }) };
111
+ });
112
+ return entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
113
+ }
76
114
  readFileAt(commitSha, path) {
77
115
  // Git object paths are top-level-relative unless explicitly prefixed with ./.
78
116
  return this.execute(['show', `${commitSha}:./${path}`], false);