@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.
Files changed (84) hide show
  1. package/README.md +149 -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.d.ts +2 -0
  15. package/dist/application/hydrator.js +40 -18
  16. package/dist/application/overlay.d.ts +3 -1
  17. package/dist/application/overlay.js +29 -9
  18. package/dist/application/saved-evidence.d.ts +2 -2
  19. package/dist/application/semantic-run.d.ts +3 -0
  20. package/dist/application/semantic-scope.d.ts +1 -1
  21. package/dist/application/semantic-scope.js +2 -2
  22. package/dist/configuration/cache.js +13 -1
  23. package/dist/configuration/config.d.ts +2 -0
  24. package/dist/configuration/config.js +6 -1
  25. package/dist/configuration/document-traversal.d.ts +7 -0
  26. package/dist/configuration/document-traversal.js +13 -0
  27. package/dist/extraction/canonical-document-links.d.ts +28 -0
  28. package/dist/extraction/canonical-document-links.js +95 -0
  29. package/dist/extraction/document-classification.d.ts +44 -0
  30. package/dist/extraction/document-classification.js +161 -0
  31. package/dist/extraction/document-evidence.d.ts +21 -0
  32. package/dist/extraction/document-evidence.js +83 -0
  33. package/dist/extraction/document-traversal.d.ts +76 -0
  34. package/dist/extraction/document-traversal.js +210 -0
  35. package/dist/extraction/document.d.ts +3 -1
  36. package/dist/extraction/document.js +3 -1
  37. package/dist/graph/authority.js +3 -0
  38. package/dist/graph/coverage.d.ts +8 -0
  39. package/dist/graph/coverage.js +96 -0
  40. package/dist/graph/document-role-description.d.ts +5 -0
  41. package/dist/graph/document-role-description.js +18 -0
  42. package/dist/graph/identity-preflight.d.ts +57 -0
  43. package/dist/graph/identity-preflight.js +71 -0
  44. package/dist/graph/query.d.ts +2 -2
  45. package/dist/graph/query.js +22 -62
  46. package/dist/graph/rdf.d.ts +4 -1
  47. package/dist/graph/rdf.js +21 -6
  48. package/dist/graph/shacl-shapes.js +26 -0
  49. package/dist/graph/validation-progress.d.ts +14 -0
  50. package/dist/graph/validation-progress.js +18 -0
  51. package/dist/index.d.ts +7 -0
  52. package/dist/index.js +6 -0
  53. package/dist/interfaces/cli.js +29 -5
  54. package/dist/interfaces/document-coverage.d.ts +9 -0
  55. package/dist/interfaces/document-coverage.js +117 -0
  56. package/dist/repository/git.d.ts +11 -0
  57. package/dist/repository/git.js +38 -0
  58. package/dist/visualization/visualization.js +21 -6
  59. package/docs/ADR-017-shacl-validation-union.md +4 -0
  60. package/docs/ADR-018-operational-semantic-cache.md +2 -2
  61. package/docs/ADR-019-semantic-failure-diagnostics.md +2 -2
  62. package/docs/ADR-020-canonical-physical-file-producers.md +2 -2
  63. package/docs/ADR-021-identity-preflight-and-validation-timing.md +84 -0
  64. package/docs/ADR-022-release-documentation.md +44 -0
  65. package/docs/ADR-023-document-coverage-preview.md +64 -0
  66. package/docs/ADR-024-implementation-coverage.md +51 -0
  67. package/docs/ADR-025-document-classification.md +78 -0
  68. package/docs/ADR-026-traversal-hydration.md +94 -0
  69. package/docs/ADR-027-validation-closeout.md +155 -0
  70. package/docs/CONSUMER-TRIAL.md +1 -1
  71. package/docs/MIGRATING-TO-WEFTLET.md +1 -1
  72. package/docs/PROVIDERS.md +17 -1
  73. package/docs/RELEASE-1.0-PLAN.md +177 -48
  74. package/docs/RELEASING.md +46 -8
  75. package/docs/ROADMAP.md +59 -28
  76. package/docs/VALIDATION.md +451 -0
  77. package/docs/benchmarks/validation-2026-10-28/README.md +25 -0
  78. package/docs/benchmarks/validation-2026-10-28/large.json +1 -0
  79. package/docs/benchmarks/validation-2026-10-28/lifecycle.json +1 -0
  80. package/docs/benchmarks/validation-2026-10-28/native.json +1 -0
  81. package/docs/benchmarks/validation-2026-10-28/rich.json +1 -0
  82. package/docs/extraction-manifest.json +73 -17
  83. package/examples/generic-typescript/document-policy.json +9 -0
  84. package/package.json +7 -4
@@ -18,6 +18,8 @@ import { loadConfig } from "../configuration/config.js";
18
18
  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
+ import { GraphIdentityConflictError } from '../graph/identity-preflight.js';
22
+ import { runDocumentCoverage } from './document-coverage.js';
21
23
  function valueAfter(args, name) {
22
24
  const index = args.indexOf(name);
23
25
  return index >= 0 ? args[index + 1] : undefined;
@@ -60,9 +62,23 @@ function formatHydrationPlan(plan) {
60
62
  `Expected cache reuse: ${plan.estimates.reusableArtifacts}; extractions: ${plan.estimates.extractions}; unknown cache entries: ${plan.estimates.unknownArtifacts}`,
61
63
  `Source/cache/budget checks: ${readiness}`,
62
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
+ }
63
76
  for (const document of plan.documents) {
64
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(', ')}`);
65
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
+ }
66
82
  for (const selection of plan.selections.filter((item) => item.status !== 'matched')) {
67
83
  lines.push(`Selection ${JSON.stringify(selection.selector)}: ${selection.status}`);
68
84
  }
@@ -82,9 +98,11 @@ function formatHydrationPlan(plan) {
82
98
  async function main() {
83
99
  const args = process.argv.slice(2);
84
100
  if (args.includes('--help') || !args.length) {
85
- 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.\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');
86
102
  return;
87
103
  }
104
+ if (await runDocumentCoverage(args, defaultConfigPath))
105
+ return;
88
106
  if (args[0] === 'init') {
89
107
  await runInit(args.slice(1));
90
108
  return;
@@ -171,7 +189,7 @@ async function main() {
171
189
  const base = valueAfter(args, '--base-snapshot');
172
190
  if (!base)
173
191
  throw new Error('overlay requires --base-snapshot <committed snapshot ID>');
174
- const result = await buildWorkingOverlay(application.repository, application.config, application.snapshotStore, application.storageRoot, base, valueAfter(args, '--agent'));
192
+ const result = await buildWorkingOverlay(application.repository, application.config, application.snapshotStore, application.storageRoot, base, valueAfter(args, '--agent'), { validationDetails: args.includes('--validation-details') });
175
193
  process.stdout.write(`${JSON.stringify(result.manifest, null, 2)}\n`);
176
194
  return;
177
195
  }
@@ -187,8 +205,11 @@ async function main() {
187
205
  headName: scope?.headName ?? application.headName,
188
206
  semanticSources: scope?.semanticSources,
189
207
  forceSemantic: args.includes('--force-semantic'),
208
+ validationDetails: args.includes('--validation-details'),
190
209
  signal: controller.signal,
191
210
  onProgress: args.includes('--quiet') ? undefined : (event) => process.stderr.write(`[context-graph] ${event.phase}${event.total === undefined ? '' : ` ${event.completed}/${event.total}`}` +
211
+ `${event.status === undefined ? '' : ` ${event.status}`}` +
212
+ `${event.factCount === undefined ? '' : ` facts=${event.factCount}`}${event.nodeCount === undefined ? '' : ` nodes=${event.nodeCount}`}` +
192
213
  `${event.sourceId === undefined ? '' : ` ${event.sourceId}`}${event.elapsedMs === undefined ? '' : ` ${event.elapsedMs} ms`}\n`),
193
214
  });
194
215
  process.stdout.write(`${JSON.stringify({
@@ -257,8 +278,11 @@ async function main() {
257
278
  throw new Error('Usage: weftlet hydrate|clean-compare|impact scan|diagnostics show --snapshot <id>|invariant check|ontology compile|visualize --output <file.html|file.dot|file.svg>');
258
279
  }
259
280
  main().catch((error) => {
260
- process.stderr.write(`${error instanceof SemanticHydrationError
261
- ? process.argv.includes('--json') ? JSON.stringify(error.report, null, 2) : error.message
262
- : error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
281
+ process.stderr.write(`${error instanceof GraphIdentityConflictError
282
+ ? process.argv.includes('--json') || process.argv.includes('--validation-details')
283
+ ? JSON.stringify(error.report, null, 2) : error.message
284
+ : error instanceof SemanticHydrationError
285
+ ? process.argv.includes('--json') ? JSON.stringify(error.report, null, 2) : error.message
286
+ : error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
263
287
  process.exitCode = 1;
264
288
  });
@@ -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);
@@ -1,7 +1,10 @@
1
1
  // Modified for standalone extraction; see docs/extraction-manifest.json.
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { dirname, extname, resolve } from 'node:path';
4
+ import { repositoryFileNode } from '@weftlet/core';
4
5
  import { contextPackEvidenceAuthority, contextPackLaneForFact } from "../graph/query.js";
6
+ import { coverageDescription } from '../graph/coverage.js';
7
+ import { documentSelectionDescription, sourceDocumentDescription } from '../graph/document-role-description.js';
5
8
  import { viewerScript } from "./viewer-interaction.js";
6
9
  import { diagnosticSummary } from "../graph/diagnostics.js";
7
10
  export const MAX_VISUALIZATION_FACTS = 200;
@@ -67,7 +70,7 @@ function visualColumn(kind, selectedLanes) {
67
70
  }
68
71
  return 'implementation';
69
72
  }
70
- function addNode(nodes, node, selectedLanes) {
73
+ function addNode(nodes, node, selectedLanes, ownValidationSource = false) {
71
74
  if (nodes.has(node.id))
72
75
  return;
73
76
  nodes.set(node.id, {
@@ -75,7 +78,7 @@ function addNode(nodes, node, selectedLanes) {
75
78
  label: node.label,
76
79
  kind: node.kind,
77
80
  cluster: String(node.attributes?.module ?? node.attributes?.layer ?? node.kind),
78
- column: visualColumn(node.kind, selectedLanes),
81
+ column: ownValidationSource && node.kind === 'Document' ? 'evidence' : visualColumn(node.kind, selectedLanes),
79
82
  });
80
83
  }
81
84
  function assertVisualizationBounds(pack) {
@@ -104,8 +107,16 @@ function visualModel(pack, sourceUrlTemplate) {
104
107
  assertVisualizationBounds(pack);
105
108
  const nodes = new Map();
106
109
  const selectedLanes = new Map();
110
+ const validationDocuments = new Set();
107
111
  const edges = [];
108
112
  for (const fact of pack.facts) {
113
+ if (fact.sourceRole === 'validation') {
114
+ const citedDocument = repositoryFileNode(fact.provenance.path);
115
+ if (citedDocument.kind === 'Document' &&
116
+ [fact.subject, fact.object].some(node => node.kind === 'Document' && node.id === citedDocument.id)) {
117
+ validationDocuments.add(citedDocument.id);
118
+ }
119
+ }
109
120
  const lane = contextPackLaneForFact(fact);
110
121
  if (!lane)
111
122
  continue;
@@ -116,8 +127,9 @@ function visualModel(pack, sourceUrlTemplate) {
116
127
  }
117
128
  }
118
129
  for (const fact of pack.facts) {
119
- addNode(nodes, fact.subject, selectedLanes.get(fact.subject.id));
120
- addNode(nodes, fact.object, selectedLanes.get(fact.object.id));
130
+ addNode(nodes, fact.subject, selectedLanes.get(fact.subject.id), validationDocuments.has(fact.subject.id));
131
+ addNode(nodes, fact.object, selectedLanes.get(fact.object.id), validationDocuments.has(fact.object.id));
132
+ const sourceDescription = sourceDocumentDescription(fact);
121
133
  edges.push({
122
134
  id: fact.id,
123
135
  from: fact.subject.id,
@@ -125,7 +137,8 @@ function visualModel(pack, sourceUrlTemplate) {
125
137
  label: fact.predicate,
126
138
  authority: contextPackEvidenceAuthority(fact),
127
139
  provenance: `${fact.provenance.path}:${fact.provenance.startLine}`,
128
- detail: `confidence ${fact.confidence}; ${fact.extractor.id}@${fact.extractor.version}; source ${fact.sourceAuthority ?? 'see citation'}; extraction ${fact.extractionReview ?? 'unspecified'}`,
140
+ detail: `confidence ${fact.confidence}; ${fact.extractor.id}@${fact.extractor.version}; source ${fact.sourceAuthority ?? 'see citation'}; extraction ${fact.extractionReview ?? 'unspecified'}` +
141
+ (sourceDescription ? `; ${sourceDescription}` : ''),
129
142
  source: fact.provenance,
130
143
  sourceUrl: sourceLink(fact.provenance, sourceUrlTemplate),
131
144
  });
@@ -398,6 +411,7 @@ export function renderContextPackHtml(pack, sourceUrlTemplate) {
398
411
  Excluded working paths: ${pack.evidenceState?.excludedWorkingChanges.length ?? 'unknown'}.
399
412
  ${pack.snapshot.overlayDigest ? `Overlay freshness: ${escapeXml(pack.evidenceState?.overlayFreshness ?? 'unknown')}.` : ''}
400
413
  This view does not validate the current diff.</p>
414
+ <p>${escapeXml(documentSelectionDescription(pack.snapshot.documentSelection))}</p>
401
415
  <p>${pack.exploration ? `${pack.exploration.returned}/${pack.exploration.reachableFacts} reachable facts on this page; max depth ${pack.exploration.maxDepth}; ${pack.exploration.truncated ? 'TRUNCATED' : 'not truncated within this scope'}${pack.exploration.depthLimitReached ? '; depth limit reached — rerun with greater --max-depth' : ''}` : 'Query completeness metadata unavailable.'}</p>
402
416
  ${pack.exploration?.continuation ? `<details><summary>Continue agent/CLI query with this token</summary><code>${escapeXml(pack.exploration.continuation)}</code></details>` : ''}
403
417
  ${pack.evidenceState?.excludedWorkingChanges.length ? `<details><summary>Excluded working changes</summary>${detailList(pack.evidenceState.excludedWorkingChanges, '')}</details>` : ''}
@@ -410,10 +424,11 @@ export function renderContextPackHtml(pack, sourceUrlTemplate) {
410
424
  <section class="panel"><h2>Runtime references</h2>${detailList(pack.runtimeEvidence.map(reference => `${reference.serviceName}: ${reference.operation ?? 'observation'} · ${reference.sourcePath} · ${reference.observedAt ?? 'time unknown'}`), 'None in this page.')}<p>Edges are only the facts returned by the agent query. References do not create extra relationships.</p></section>
411
425
  <section class="panel">
412
426
  <h2>Coverage</h2>
427
+ <p>${escapeXml(coverageDescription(pack.coverage))}</p>
413
428
  ${Object.entries(pack.coverage.counts)
414
429
  .map(([lane, count]) => `<span class="metric">${escapeXml(lane)}: ${count}</span>`)
415
430
  .join('')}
416
- ${detailList(pack.coverage.gaps, 'No Context Pack lane gaps detected.')}
431
+ ${detailList(pack.coverage.gaps, 'No coverage gaps detected on this returned page.')}
417
432
  </section>
418
433
  <section class="panel">
419
434
  <h2>Governing invariants</h2>
@@ -3,6 +3,10 @@
3
3
  Status: accepted and implemented in the focused 0.3.0 release; absent from 0.2.1.
4
4
  See [validation](VALIDATION.md) for checks and recorded publication evidence.
5
5
 
6
+ Follow-up: [ADR 027](ADR-027-validation-closeout.md) records the completed T05 engineering
7
+ evaluation and retains full direct validation. The measurements and remaining-work section below
8
+ describe the original focused release; private consumer acceptance remains separate.
9
+
6
10
  ## Problem and measured mechanism
7
11
 
8
12
  The canonical RDF dataset has one named relationship graph per assertion, with assertion records,
@@ -1,7 +1,7 @@
1
1
  # ADR-018: Preserve Codex artifacts across operational configuration changes
2
2
 
3
- Status: accepted and implemented in development source after 0.3.0; release evidence is recorded
4
- in [validation](VALIDATION.md). This increment does not claim a new npm publication.
3
+ Status: accepted and shipped in 0.4.0; release and validation evidence is recorded in
4
+ [validation](VALIDATION.md).
5
5
 
6
6
  ## Problem
7
7
 
@@ -1,7 +1,7 @@
1
1
  # ADR-019: Safe semantic failure diagnostics and settled progress
2
2
 
3
- Status: implemented in development source after 0.3.0; validation and publication are recorded
4
- separately in [validation](VALIDATION.md). This decision does not change extraction identity.
3
+ Status: shipped in 0.4.0; validation and publication are recorded in [validation](VALIDATION.md).
4
+ This decision does not change extraction identity.
5
5
 
6
6
  ## Problem
7
7
 
@@ -1,7 +1,7 @@
1
1
  # ADR-020: One physical-file policy for deterministic producers and adapters
2
2
 
3
- Status: implemented in development source after 0.3.0; release/consumer acceptance is recorded
4
- separately in [validation](VALIDATION.md).
3
+ Status: upstream fix shipped in 0.4.0; release evidence and pending consumer adapter/hydration
4
+ acceptance are recorded separately in [validation](VALIDATION.md).
5
5
 
6
6
  ## Reported defect and ownership
7
7
 
@@ -0,0 +1,84 @@
1
+ # ADR-021: Early identity conflicts and honest validation phase timing
2
+
3
+ Status: shipped in 0.5.0 after local/hosted checks and independent archive review; release evidence
4
+ is in [validation](VALIDATION.md). Consumer acceptance remains a separate checkpoint.
5
+ This is the bounded T05a increment. Follow-up [ADR 027](ADR-027-validation-closeout.md) records
6
+ the broader T05 engineering evaluation; the remaining-work section below describes this original
7
+ increment. Private consumer performance acceptance remains separate.
8
+
9
+ ## Decision
10
+
11
+ Before ontology compilation, invariant evaluation or RDF construction, scan every assembled
12
+ fact's subject and object identity for multiple kinds. The scan covers semantic, document,
13
+ static, adapter, routing and runtime facts, including conceptual identities. Working overlays
14
+ run the same check over retained and newly extracted facts before RDF construction/publication.
15
+ No fact, kind, ID, citation or authority is changed or discarded by the check.
16
+ This runs after semantic/static extraction; it is separate from no-model `hydrate --plan`.
17
+
18
+ `inspectGraphIdentities(facts, { repositoryId, includeDetails? })` returns a report;
19
+ `assertGraphIdentities` throws `GraphIdentityConflictError` on conflicts. Both are public toolkit
20
+ exports. A passing report has `passed: true` and **`shaclValidated: false`**. Consistent kinds
21
+ alone do not establish canonical classification, correct intent, valid citations or conformance.
22
+ The existing full RDF/SHACL path and shapes remain mandatory for successful publication. Direct
23
+ `RdfEvidenceGraph.validate()` and the saved `validate` command still run all checks and return
24
+ their existing verdict format. There is no validation cache, incremental validator or backend change.
25
+
26
+ The two-pass scan is linear in fact occurrences. The common conflict-free path creates no
27
+ per-evidence diagnostic records. On failure it reports every conflicting identity and occurrence,
28
+ including both ends of a self-reference, repeated fact IDs and late conflicts. Counts are complete;
29
+ there is no hidden document, hop or diagnostic cutoff. Reports retain input indices and side so
30
+ duplicate assertion IDs cannot conceal a producer's occurrence.
31
+
32
+ ## Privacy and attribution
33
+
34
+ There was no general configurable diagnostic privacy policy. This increment therefore makes
35
+ local detail an explicit invocation option, without adding an extraction/configuration field.
36
+ Default reports contain repository-scoped opaque node, fact, source and extractor references,
37
+ counts and recognized kind names. Unknown kind strings are hashed as well. References correlate
38
+ records; they are not encryption or proof that a low-entropy identifier cannot be guessed.
39
+
40
+ `hydrate --json --quiet` emits a typed identity-failure report on stderr with empty stdout.
41
+ Use `--validation-details` on `hydrate` or `overlay` only when local identities and citations
42
+ may be displayed. That option includes exact node/fact IDs, extractor identity/version, citation
43
+ path/lines/digest/commit, confidence and the original optional authority/review fields. It does
44
+ not include labels, node attributes, source text, prompts or raw provider output. Absent authority
45
+ fields stay absent; source approval is never promoted into assertion approval.
46
+
47
+ The programmatic equivalent is `HydrationOptions.validationDetails` or the overlay's final
48
+ options argument. Detail changes do not enter snapshot, provider or semantic-cache identities.
49
+ The error message stays safe even when its report includes explicitly requested local details.
50
+ No diagnostic file is written automatically. Existing non-identity SHACL errors retain their
51
+ existing detailed behavior; this is not a claim that every command/error has been redacted.
52
+
53
+ ## Timing and failure preservation
54
+
55
+ Hydration reports started/completed/failed boundaries and elapsed wall time for identity
56
+ preflight, RDF construction, union construction and full SHACL. Counts describe graph size,
57
+ not validator completion. A completed SHACL phase means the validator returned, including a
58
+ nonconforming verdict. `RdfEvidenceGraph.validate({ onProgress })` exposes its union/SHACL phases.
59
+
60
+ The synchronous JavaScript work can block same-thread timers. These events are not heartbeats,
61
+ percentages, new timeout controls or interruptible worker execution. Worker isolation remains a
62
+ separate decision. The public profiling harness records preflight and validation phase timings
63
+ alongside its existing construction, resource and runtime measurements.
64
+
65
+ Failure preserves completed semantic artifacts and prior snapshots/heads. Correcting and
66
+ versioning an adapter still requires a fresh assembly and full validation, but compatible
67
+ semantic artifacts can be reused. This preflight does not rewrite cached model assertions or
68
+ choose an authoritative business rule. Evidence disagreements must be resolved at their owner.
69
+
70
+ No extraction/assembly version changes are needed: accepted graph facts, cache identities,
71
+ RDF serialization and conformance constraints are unchanged. The check rejects an existing
72
+ single-kind violation earlier; it does not make old snapshots satisfy current validation.
73
+
74
+ ## Evidence and remaining work
75
+
76
+ Tests cover full cross-source attribution, conceptual and physical nodes, exact authority,
77
+ default privacy, late conflicts, duplicates/self-references, immutable artifacts/snapshots,
78
+ corrected retries and non-kind failures that still reach full SHACL. CLI and packed-consumer
79
+ cases use actual TypeScript SCIP and public adapters. Existing positive/negative named-dataset
80
+ SHACL equivalence tests remain unchanged except for an additional timing/parity case.
81
+
82
+ Broader T05 work remains: worker-based progress/cancellation, repeated/changed-input performance
83
+ evaluation, and evidence-driven decisions about conformance-result reuse or incremental/native
84
+ backends. This increment does not infer private consumer performance from public fixtures.
@@ -0,0 +1,44 @@
1
+ # ADR-022: Versioned installation documentation checked against release archives
2
+
3
+ Status: implemented for T06 after 0.5.0; validation is recorded separately in
4
+ [validation](VALIDATION.md). This changes release tooling and packaged documentation, not graph semantics.
5
+
6
+ ## Decision
7
+
8
+ The two package manifests own the version of the current checkout/archive. Marked sections in
9
+ the toolkit README, core README and release guide are generated from those manifests. They do
10
+ not claim a version is already published: a Changesets version PR can precede registry availability.
11
+ Publication evidence remains in the validation record and package-specific releases.
12
+
13
+ `scripts/release-documentation.mjs` uses Node built-ins. It is usable by the publish job, which
14
+ installs no project dependencies. Versioning regenerates the marked sections after Changesets
15
+ updates the manifests and before the extraction inventory is refreshed. Missing, duplicate or
16
+ malformed markers fail rather than causing unrelated prose to be overwritten.
17
+
18
+ Source checks compare the complete generated sections and check explicit package pins and
19
+ Weftlet archive names in active installation examples. The maintained scope is the two READMEs,
20
+ `docs/RELEASING.md`, `docs/MIGRATING-TO-WEFTLET.md` and `docs/CONSUMER-TRIAL.md`.
21
+ Feature introductions, earlier release records and historical inspection commands remain intact.
22
+ Historical installation fences require an explicit `weftlet-history` classification; this is a
23
+ reviewed documentation exception, not an automatic exemption based on an old version number.
24
+
25
+ Packed checks read the actual two tarballs. Package names, versions, archive filenames and the
26
+ toolkit's core dependency must match the release group. Required operational documents must be
27
+ present and match the reviewed source, allowing equivalent LF/CRLF checkout representations.
28
+ The same documentation checks run on those packed documents. Both archives are checked before
29
+ any publish operation, including when publication resumes from prepared artifacts.
30
+
31
+ The fresh-consumer smoke uses constrained argument arrays obtained from the checked current
32
+ examples. It installs the documented archive pair and runs the documented pinned setup and
33
+ configuration-validation commands against a disposable registry serving those archives. It
34
+ never evaluates arbitrary Markdown as shell code and makes no model requests.
35
+
36
+ ## Boundaries
37
+
38
+ This is a release-documentation check, not a natural-language verifier or general shell parser.
39
+ New current version statements belong in the generated sections. New installation guides must
40
+ be added to the explicit operational-document scope to receive the same checks. Historical
41
+ validation records, changelogs, feature-introduction versions and private consumer documentation
42
+ are not automatically rewritten. Existing immutable npm archives are unchanged.
43
+
44
+ T07 document selection/authority and broader T05 validation progress/reuse remain separate.
@@ -0,0 +1,64 @@
1
+ # ADR-023: Prospective committed document coverage
2
+
3
+ Status: implemented as T07a after T06; source validation and publication are separate in
4
+ [validation](VALIDATION.md). T07b classification/evidence and T07c hydration integration remain planned.
5
+
6
+ ## Decision
7
+
8
+ `weftlet documents plan` and `planDocumentCoverage()` inspect the committed Markdown-link closure
9
+ from explicit entry points. The report is `document-coverage-plan`, with `mode: prospective` and
10
+ `changesHydrationSelection: false`. It is separate from `hydrate --plan`, whose inputs and cache
11
+ estimates continue to describe actual hydration. No provider, adapter, indexer, cache, snapshot,
12
+ runtime source or ontology compiler is initialized or written by the new operation.
13
+
14
+ Entry points, exact scope roots and read limits are operation arguments. This increment adds no
15
+ hydration configuration fields and changes no cache partitions, extractor versions or graph
16
+ assembly identities. Existing config supplies repository identity, explicit approval/evidence-root
17
+ labels and semantic budgets for comparison. This avoids making an unused traversal setting
18
+ invalidate current snapshots or cached artifacts.
19
+
20
+ The new NUL-delimited Git tree reader preserves exact committed paths and records type, mode,
21
+ object ID and byte size, including directories, symlinks and gitlinks. It scopes correctly to a
22
+ nested repository root. Existing Git listing/extraction APIs are unchanged. Invalid UTF-8 tree
23
+ paths or malformed metadata fail closed instead of silently disappearing from the index.
24
+
25
+ A separate canonical-link parser follows direct Markdown links and used reference definitions.
26
+ It retains source lines, definition/use positions and conflicting definitions. It does not change
27
+ the graph linker's existing treatment of bare and inline-code paths. Images, code examples, HTML
28
+ and unused definitions do not become traversal edges. CommonMark-compatible links in Markdown,
29
+ MDX and text files are supported; this is not a complete MDX/HTML parser.
30
+
31
+ Relative paths resolve against their source document, leading `/` means repository root, and
32
+ percent encoding and fragment/query removal are explicit. No basename/README guessing or remote
33
+ fetch occurs. Malformed/escaping targets, scope exclusions, missing files, directories, symlinks,
34
+ submodules and ambiguous references are reported distinctly. Scope roots bound document traversal;
35
+ existing regular code/non-document targets outside those roots can still be classified without
36
+ reading their contents. Resolved code links, external URLs
37
+ and same-document fragments remain visible without being traversed. Visited-path deduplication
38
+ terminates cycles while retaining every reaching-link occurrence.
39
+
40
+ ## Coverage and limits
41
+
42
+ Traversal computes reachable supported documents before comparing extraction budgets. The
43
+ existing `semantic.maxDocuments`/`maxSourceBytes` values apply only to that comparison; they never
44
+ trim the traversal. Candidates include validation documents at this stage, so totals are not a
45
+ promise of future model work. Missing links and unread descendants make coverage incomplete.
46
+ `ready` requires complete supported local coverage and a passing budget comparison; it does not
47
+ establish approved requirements, semantic quality, complete product coverage or hydration readiness.
48
+
49
+ Separate read-safety limits default to 1,000 documents, 16 MiB total and 1 MiB per document,
50
+ with ceilings of 10,000 documents, 64 MiB and 4 MiB respectively. Committed blob sizes are checked
51
+ before reads; failed attempts count toward read limits. Discovered unread paths remain in the
52
+ frontier and their descendants are explicitly unknown. Git metadata output is bounded to 64 MiB.
53
+ The current string reader conservatively rejects NUL, byte-size mismatches and U+FFFD, including
54
+ a literal replacement character, because it cannot prove lossless decoding in that case.
55
+
56
+ Reports include commit, current configuration digest, coverage digest, paths, sizes, content
57
+ digests, citations, issues and excluded working changes, but no document bodies. They are local
58
+ diagnostic reports and may contain private paths/link destinations. Source approval matches
59
+ remain explicit and do not propagate through links; prospective assertion review stays unreviewed.
60
+ Evidence-root labels do not infer rule ownership or approve source assertions.
61
+
62
+ Before traversal can drive extraction, T07b must preserve validation documents through a separate
63
+ deterministic evidence path. T07c must connect the same selection to hydration, cache recovery and
64
+ freshness checks, with real cross-source positive/negative fixtures and rename/delete behavior.