@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.
- package/README.md +139 -14
- package/dist/application/cache-adoption.d.ts +2 -1
- package/dist/application/cache-adoption.js +12 -5
- package/dist/application/document-coverage.d.ts +28 -0
- package/dist/application/document-coverage.js +58 -0
- package/dist/application/document-overlay.d.ts +14 -0
- package/dist/application/document-overlay.js +31 -0
- package/dist/application/extraction-identity.d.ts +2 -1
- package/dist/application/extraction-identity.js +7 -0
- package/dist/application/hydration-inputs.d.ts +15 -0
- package/dist/application/hydration-inputs.js +55 -0
- package/dist/application/hydration-plan.d.ts +7 -0
- package/dist/application/hydration-plan.js +18 -5
- package/dist/application/hydrator.js +26 -14
- package/dist/application/overlay.js +26 -8
- package/dist/application/saved-evidence.d.ts +2 -2
- package/dist/application/semantic-scope.d.ts +1 -1
- package/dist/application/semantic-scope.js +2 -2
- package/dist/configuration/cache.js +13 -1
- package/dist/configuration/config.d.ts +2 -0
- package/dist/configuration/config.js +5 -0
- package/dist/configuration/document-traversal.d.ts +7 -0
- package/dist/configuration/document-traversal.js +13 -0
- package/dist/extraction/canonical-document-links.d.ts +28 -0
- package/dist/extraction/canonical-document-links.js +95 -0
- package/dist/extraction/document-classification.d.ts +44 -0
- package/dist/extraction/document-classification.js +161 -0
- package/dist/extraction/document-evidence.d.ts +21 -0
- package/dist/extraction/document-evidence.js +83 -0
- package/dist/extraction/document-traversal.d.ts +76 -0
- package/dist/extraction/document-traversal.js +210 -0
- package/dist/extraction/document.d.ts +3 -1
- package/dist/extraction/document.js +3 -1
- package/dist/extraction/ontology.js +2 -2
- package/dist/extraction/static.js +25 -20
- package/dist/graph/authority.js +3 -0
- package/dist/graph/coverage.d.ts +8 -0
- package/dist/graph/coverage.js +96 -0
- package/dist/graph/document-role-description.d.ts +5 -0
- package/dist/graph/document-role-description.js +18 -0
- package/dist/graph/query.d.ts +2 -2
- package/dist/graph/query.js +22 -62
- package/dist/graph/rdf.js +11 -0
- package/dist/graph/shacl-shapes.js +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infrastructure/artifact-paths.d.ts +4 -0
- package/dist/infrastructure/artifact-paths.js +14 -0
- package/dist/interfaces/cli.js +18 -1
- package/dist/interfaces/document-coverage.d.ts +9 -0
- package/dist/interfaces/document-coverage.js +117 -0
- package/dist/repository/git.d.ts +11 -0
- package/dist/repository/git.js +38 -0
- package/dist/storage/storage.js +41 -13
- package/dist/visualization/visualization.js +21 -6
- package/docs/ADR-017-shacl-validation-union.md +4 -0
- package/docs/ADR-021-identity-preflight-and-validation-timing.md +5 -2
- package/docs/ADR-022-release-documentation.md +44 -0
- package/docs/ADR-023-document-coverage-preview.md +64 -0
- package/docs/ADR-024-implementation-coverage.md +51 -0
- package/docs/ADR-025-document-classification.md +78 -0
- package/docs/ADR-026-traversal-hydration.md +94 -0
- package/docs/ADR-027-validation-closeout.md +155 -0
- package/docs/ADR-028-bounded-artifact-paths.md +58 -0
- package/docs/CONSUMER-TRIAL.md +7 -5
- package/docs/GRAPHIFY-EVALUATION.md +70 -0
- package/docs/MIGRATING-TO-WEFTLET.md +1 -1
- package/docs/PRE-1.0-BASELINE.md +94 -0
- package/docs/PROVIDERS.md +10 -1
- package/docs/RELEASE-1.0-PLAN.md +145 -23
- package/docs/RELEASING.md +46 -8
- package/docs/ROADMAP.md +48 -16
- package/docs/VALIDATION.md +390 -0
- package/docs/assessments/pre-1.0-2026-10-28.json +200 -0
- package/docs/benchmarks/validation-2026-10-28/README.md +25 -0
- package/docs/benchmarks/validation-2026-10-28/large.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/lifecycle.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/native.json +1 -0
- package/docs/benchmarks/validation-2026-10-28/rich.json +1 -0
- package/docs/extraction-manifest.json +78 -20
- package/examples/generic-typescript/document-policy.json +9 -0
- 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
|
+
}
|
package/dist/repository/git.d.ts
CHANGED
|
@@ -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>;
|
package/dist/repository/git.js
CHANGED
|
@@ -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);
|
package/dist/storage/storage.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
95
|
+
return readWithLegacyPath(snapshotPath(this.root, snapshotId), snapshotPath(this.root, snapshotId, true));
|
|
77
96
|
}
|
|
78
97
|
async readHead(headName) {
|
|
79
|
-
const
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
4
|
-
|
|
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.
|