@weftlet/toolkit 0.5.0 → 0.6.1

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 (82) 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/extraction/ontology.js +2 -2
  35. package/dist/extraction/static.js +25 -20
  36. package/dist/graph/authority.js +3 -0
  37. package/dist/graph/coverage.d.ts +8 -0
  38. package/dist/graph/coverage.js +96 -0
  39. package/dist/graph/document-role-description.d.ts +5 -0
  40. package/dist/graph/document-role-description.js +18 -0
  41. package/dist/graph/query.d.ts +2 -2
  42. package/dist/graph/query.js +22 -62
  43. package/dist/graph/rdf.js +11 -0
  44. package/dist/graph/shacl-shapes.js +26 -0
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.js +4 -0
  47. package/dist/infrastructure/artifact-paths.d.ts +4 -0
  48. package/dist/infrastructure/artifact-paths.js +14 -0
  49. package/dist/interfaces/cli.js +18 -1
  50. package/dist/interfaces/document-coverage.d.ts +9 -0
  51. package/dist/interfaces/document-coverage.js +117 -0
  52. package/dist/repository/git.d.ts +11 -0
  53. package/dist/repository/git.js +38 -0
  54. package/dist/storage/storage.js +41 -13
  55. package/dist/visualization/visualization.js +21 -6
  56. package/docs/ADR-017-shacl-validation-union.md +4 -0
  57. package/docs/ADR-021-identity-preflight-and-validation-timing.md +5 -2
  58. package/docs/ADR-022-release-documentation.md +44 -0
  59. package/docs/ADR-023-document-coverage-preview.md +64 -0
  60. package/docs/ADR-024-implementation-coverage.md +51 -0
  61. package/docs/ADR-025-document-classification.md +78 -0
  62. package/docs/ADR-026-traversal-hydration.md +94 -0
  63. package/docs/ADR-027-validation-closeout.md +155 -0
  64. package/docs/ADR-028-bounded-artifact-paths.md +58 -0
  65. package/docs/CONSUMER-TRIAL.md +7 -5
  66. package/docs/GRAPHIFY-EVALUATION.md +70 -0
  67. package/docs/MIGRATING-TO-WEFTLET.md +1 -1
  68. package/docs/PRE-1.0-BASELINE.md +94 -0
  69. package/docs/PROVIDERS.md +10 -1
  70. package/docs/RELEASE-1.0-PLAN.md +145 -23
  71. package/docs/RELEASING.md +46 -8
  72. package/docs/ROADMAP.md +48 -16
  73. package/docs/VALIDATION.md +390 -0
  74. package/docs/assessments/pre-1.0-2026-10-28.json +200 -0
  75. package/docs/benchmarks/validation-2026-10-28/README.md +25 -0
  76. package/docs/benchmarks/validation-2026-10-28/large.json +1 -0
  77. package/docs/benchmarks/validation-2026-10-28/lifecycle.json +1 -0
  78. package/docs/benchmarks/validation-2026-10-28/native.json +1 -0
  79. package/docs/benchmarks/validation-2026-10-28/rich.json +1 -0
  80. package/docs/extraction-manifest.json +78 -20
  81. package/examples/generic-typescript/document-policy.json +9 -0
  82. package/package.json +7 -4
@@ -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,8 +1,8 @@
1
1
  // Modified for standalone extraction; see docs/extraction-manifest.json.
2
2
  import { mkdir, readFile, writeFile, link, rm } from 'node:fs/promises';
3
- import { randomUUID } from 'node:crypto';
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
4
  import { atomicRename } from './atomic-rename.js';
5
+ import { physicalArtifactKey, temporaryArtifactPath } from '../infrastructure/artifact-paths.js';
6
6
  async function readJson(path) {
7
7
  try {
8
8
  return JSON.parse(await readFile(path, 'utf8'));
@@ -13,9 +13,21 @@ async function readJson(path) {
13
13
  throw error;
14
14
  }
15
15
  }
16
+ async function readWithLegacyPath(path, legacyPath) {
17
+ const current = await readJson(path);
18
+ if (current !== undefined || path === legacyPath)
19
+ return current;
20
+ // No old file could have this component on supported filesystems. Do not attempt an
21
+ // overlong fallback after a normal cache miss; other I/O/corruption errors still propagate.
22
+ if (Buffer.byteLength(basename(legacyPath)) > 255)
23
+ return undefined;
24
+ if (process.platform === 'win32' && /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(basename(legacyPath)))
25
+ return undefined;
26
+ return readJson(legacyPath);
27
+ }
16
28
  async function atomicJson(path, value) {
17
29
  await mkdir(dirname(path), { recursive: true });
18
- const temporary = `${path}.${randomUUID()}.tmp`;
30
+ const temporary = temporaryArtifactPath(path);
19
31
  try {
20
32
  await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
21
33
  await atomicRename(temporary, path);
@@ -29,7 +41,7 @@ function safeKey(key) {
29
41
  throw new Error('Unsafe storage identifier');
30
42
  return key;
31
43
  }
32
- function snapshotPath(root, snapshotId) {
44
+ function snapshotPath(root, snapshotId, legacy = false) {
33
45
  if (snapshotId.length > 256 ||
34
46
  !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(snapshotId) ||
35
47
  snapshotId === '.' ||
@@ -37,7 +49,7 @@ function snapshotPath(root, snapshotId) {
37
49
  throw new Error('snapshotId must be a safe storage identifier');
38
50
  }
39
51
  const directory = resolve(root, 'snapshots');
40
- const candidate = resolve(directory, `${snapshotId}.json`);
52
+ const candidate = resolve(directory, `${legacy ? snapshotId : physicalArtifactKey(snapshotId)}.json`);
41
53
  const pathFromDirectory = relative(directory, candidate);
42
54
  if (pathFromDirectory === '..' ||
43
55
  pathFromDirectory.startsWith(`..${sep}`) ||
@@ -53,8 +65,15 @@ export class FileSnapshotStore {
53
65
  }
54
66
  async write(snapshot) {
55
67
  const destination = snapshotPath(this.root, snapshot.manifest.snapshotId);
68
+ const prior = await this.read(snapshot.manifest.snapshotId);
69
+ if (prior) {
70
+ if (prior.manifest.contentDigest !== snapshot.manifest.contentDigest) {
71
+ throw new Error(`Refusing to overwrite immutable snapshot ${snapshot.manifest.snapshotId} with different content`);
72
+ }
73
+ return; // Preserve an existing legacy file without copying or rewriting its bytes.
74
+ }
56
75
  await mkdir(dirname(destination), { recursive: true });
57
- const temporary = `${destination}.${randomUUID()}.tmp`;
76
+ const temporary = temporaryArtifactPath(destination);
58
77
  try {
59
78
  await writeFile(temporary, JSON.stringify(snapshot), { flag: 'wx' });
60
79
  // Linking a complete file publishes atomically without ever replacing a winner.
@@ -73,16 +92,17 @@ export class FileSnapshotStore {
73
92
  }
74
93
  }
75
94
  read(snapshotId) {
76
- return readJson(snapshotPath(this.root, snapshotId));
95
+ return readWithLegacyPath(snapshotPath(this.root, snapshotId), snapshotPath(this.root, snapshotId, true));
77
96
  }
78
97
  async readHead(headName) {
79
- const head = await readJson(join(this.root, 'heads', `${safeKey(headName)}.json`));
98
+ const key = safeKey(headName);
99
+ const head = await readWithLegacyPath(join(this.root, 'heads', `${physicalArtifactKey(key)}.json`), join(this.root, 'heads', `${key}.json`));
80
100
  return head ? this.read(head.snapshotId) : undefined;
81
101
  }
82
102
  async updateHead(headName, snapshotId) {
83
103
  if (!(await this.read(snapshotId)))
84
104
  throw new Error('Cannot point a head to an absent snapshot');
85
- return atomicJson(join(this.root, 'heads', `${safeKey(headName)}.json`), { snapshotId });
105
+ return atomicJson(join(this.root, 'heads', `${physicalArtifactKey(safeKey(headName))}.json`), { snapshotId });
86
106
  }
87
107
  }
88
108
  export class SemanticArtifactStore {
@@ -97,18 +117,26 @@ export class SemanticArtifactStore {
97
117
  }
98
118
  /** Only explicitly supplied roots are consulted; never enumerate other consumers' profiles. */
99
119
  async readEntry(key) {
100
- const filename = `${safeKey(key)}.json`;
120
+ const filename = `${physicalArtifactKey(safeKey(key))}.json`;
101
121
  for (const [index, root] of [this.root, ...this.legacyRoots].entries()) {
102
- const artifact = await readJson(join(root, 'artifacts', 'semantic', filename));
122
+ const artifact = await readWithLegacyPath(join(root, 'artifacts', 'semantic', filename), join(root, 'artifacts', 'semantic', `${key}.json`));
103
123
  if (artifact !== undefined)
104
124
  return { artifact, origin: index === 0 ? 'primary' : 'legacy' };
105
125
  }
106
126
  return undefined;
107
127
  }
108
128
  async write(artifact) {
109
- const path = join(this.root, 'artifacts', 'semantic', `${safeKey(artifact.key)}.json`);
129
+ const key = safeKey(artifact.key);
130
+ const path = join(this.root, 'artifacts', 'semantic', `${physicalArtifactKey(key)}.json`);
131
+ const prior = await readWithLegacyPath(path, join(this.root, 'artifacts', 'semantic', `${key}.json`));
132
+ if (prior) {
133
+ if (prior.outputDigest !== artifact.outputDigest) {
134
+ throw new Error('Concurrent semantic artifact differs; retry with explicit force refresh');
135
+ }
136
+ return;
137
+ }
110
138
  await mkdir(dirname(path), { recursive: true });
111
- const temporary = `${path}.${randomUUID()}.tmp`;
139
+ const temporary = temporaryArtifactPath(path);
112
140
  try {
113
141
  await writeFile(temporary, JSON.stringify(artifact), { flag: 'wx' });
114
142
  await link(temporary, path);
@@ -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,10 @@
1
1
  # ADR-021: Early identity conflicts and honest validation phase timing
2
2
 
3
- Status: implemented in development source after 0.4.0. Publication and consumer acceptance
4
- are separate checkpoints in [validation](VALIDATION.md). This is the bounded T05a increment.
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.
5
8
 
6
9
  ## Decision
7
10
 
@@ -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.
@@ -0,0 +1,51 @@
1
+ # ADR-024: Report evidence coverage independently of display groups
2
+
3
+ Status: implemented after T07a; validation and publication are recorded separately in
4
+ [validation](VALIDATION.md). This is a query-reporting correction, not T07b document classification.
5
+
6
+ ## Problem
7
+
8
+ The consumer reports returned code relationships with zero implementation coverage. Public
9
+ reproductions using the actual contract extractor and real SCIP confirm the defect: a file's
10
+ definition or consumption of a Contract is placed in the contracts display group. Coverage used
11
+ the exclusive display-group counts, so these code relationships could not count as implementation.
12
+ The inverse also occurred: unapproved document/model facts in the fallback implementation group
13
+ could suppress an implementation gap. Page-local absence was described as query-wide absence.
14
+
15
+ ## Decision
16
+
17
+ Keep primary display groups, returned facts, ranking, pagination and authority unchanged. Compute
18
+ implementation coverage from the returned evidence independently of display placement. A production
19
+ code relationship can count toward both contract and implementation coverage without appearing twice
20
+ in the returned facts. The other coverage categories retain their existing classification.
21
+
22
+ New reports include `coverage.scope: "returned-page"` and `countsAreNonExclusive: true`.
23
+ Both fields are optional in core for compatibility with older Context Packs. Counts are relationship
24
+ counts, not percentages, totals of display sections, product completeness or proof of correct behavior.
25
+ Markdown and HTML explain overlap. Missing-evidence messages refer to the returned page; continuation
26
+ and traversal bounds can leave further evidence outside it. Existing source diagnostics remain intact.
27
+
28
+ An implementation relationship must have effective `implemented` authority, a non-document,
29
+ non-validation citation, and no unreviewed extraction marker. Current document/model, runtime,
30
+ draft and validation evidence cannot qualify by merely mentioning a code file. Supported document
31
+ extensions and test/spec names use the canonical core file policy. Conventional test/validation
32
+ directories, including `__tests__` and `__mocks__`, are excluded from production coverage.
33
+
34
+ Legacy deterministic adapters that omitted authority retain the existing authority fallback only
35
+ when a canonical physical Component endpoint matches the cited file. An adapter explicitly marking
36
+ a fact `implemented` may also use logical Component/Provider identities with a production citation.
37
+ Unmarked legacy provider output cannot be distinguished from adapter output when their metadata is
38
+ identical; no new claim of provider verification or execution is made. Saved fact authority is never
39
+ rewritten by this reporting classification.
40
+
41
+ ## Compatibility and validation
42
+
43
+ No extractor, prompt, semantic artifact, assembly or ranking identity changes. Existing immutable
44
+ snapshots immediately benefit when queried by the updated toolkit; no model run or hydration is
45
+ needed. Historical Context Packs keep their original reports unless queried again. Existing clients
46
+ should inspect the new metadata rather than summing counts as a disjoint total.
47
+
48
+ Regressions use actual contract extraction, real TypeScript SCIP, strict SHACL and immutable
49
+ round-tripped facts. They cover overlapping counts, page continuation, API/MCP/Markdown/HTML
50
+ consistency, document/model/test negatives and retained source diagnostics. Installed-package smoke
51
+ checks query actual extracted facts rather than constructing coverage values by hand.