@weftlet/toolkit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/NOTICE +6 -0
- package/README.md +155 -2
- package/THIRD_PARTY_NOTICES.md +610 -0
- package/dist/application/agent-context.d.ts +23 -0
- package/dist/application/agent-context.js +94 -0
- package/dist/application/application.d.ts +23 -0
- package/dist/application/application.js +91 -0
- package/dist/application/hydrator.d.ts +49 -0
- package/dist/application/hydrator.js +275 -0
- package/dist/application/overlay.d.ts +6 -0
- package/dist/application/overlay.js +110 -0
- package/dist/application/saved-evidence.d.ts +38 -0
- package/dist/application/saved-evidence.js +59 -0
- package/dist/application/snapshot-checks.d.ts +16 -0
- package/dist/application/snapshot-checks.js +51 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +4 -0
- package/dist/configuration/cache.d.ts +2 -0
- package/dist/configuration/cache.js +10 -0
- package/dist/configuration/config.d.ts +79 -0
- package/dist/configuration/config.js +165 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +2 -0
- package/dist/extraction/codex-auth.d.ts +12 -0
- package/dist/extraction/codex-auth.js +74 -0
- package/dist/extraction/document.d.ts +5 -0
- package/dist/extraction/document.js +54 -0
- package/dist/extraction/ontology.d.ts +32 -0
- package/dist/extraction/ontology.js +164 -0
- package/dist/extraction/providers.d.ts +8 -0
- package/dist/extraction/providers.js +58 -0
- package/dist/extraction/runtime-evidence.d.ts +2 -0
- package/dist/extraction/runtime-evidence.js +84 -0
- package/dist/extraction/semantic-citations.d.ts +7 -0
- package/dist/extraction/semantic-citations.js +27 -0
- package/dist/extraction/semantic-process.d.ts +11 -0
- package/dist/extraction/semantic-process.js +68 -0
- package/dist/extraction/semantic.d.ts +73 -0
- package/dist/extraction/semantic.js +414 -0
- package/dist/extraction/static.d.ts +52 -0
- package/dist/extraction/static.js +322 -0
- package/dist/extraction/telemetry.d.ts +19 -0
- package/dist/extraction/telemetry.js +259 -0
- package/dist/governance/governance.d.ts +60 -0
- package/dist/governance/governance.js +209 -0
- package/dist/graph/authority.d.ts +2 -0
- package/dist/graph/authority.js +13 -0
- package/dist/graph/diagnostics.d.ts +5 -0
- package/dist/graph/diagnostics.js +61 -0
- package/dist/graph/query.d.ts +16 -0
- package/dist/graph/query.js +373 -0
- package/dist/graph/rdf.d.ts +26 -0
- package/dist/graph/rdf.js +144 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/infrastructure/module-resolution.d.ts +2 -0
- package/dist/infrastructure/module-resolution.js +10 -0
- package/dist/infrastructure/process.d.ts +5 -0
- package/dist/infrastructure/process.js +27 -0
- package/dist/infrastructure/util.d.ts +1 -0
- package/dist/infrastructure/util.js +2 -0
- package/dist/interfaces/cli.d.ts +4 -0
- package/dist/interfaces/cli.js +197 -0
- package/dist/interfaces/doctor.d.ts +25 -0
- package/dist/interfaces/doctor.js +49 -0
- package/dist/interfaces/init.d.ts +24 -0
- package/dist/interfaces/init.js +154 -0
- package/dist/interfaces/mcp-facade.d.ts +3 -0
- package/dist/interfaces/mcp-facade.js +4 -0
- package/dist/interfaces/mcp.d.ts +2 -0
- package/dist/interfaces/mcp.js +81 -0
- package/dist/interfaces/request.d.ts +2 -0
- package/dist/interfaces/request.js +59 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +4 -0
- package/dist/repository/committed-tree.d.ts +2 -0
- package/dist/repository/committed-tree.js +35 -0
- package/dist/repository/git.d.ts +26 -0
- package/dist/repository/git.js +163 -0
- package/dist/repository/glob.d.ts +2 -0
- package/dist/repository/glob.js +46 -0
- package/dist/repository/working-content.d.ts +3 -0
- package/dist/repository/working-content.js +34 -0
- package/dist/storage/atomic-rename.d.ts +2 -0
- package/dist/storage/atomic-rename.js +22 -0
- package/dist/storage/storage.d.ts +15 -0
- package/dist/storage/storage.js +115 -0
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +2 -0
- package/dist/visualization/viewer-interaction.d.ts +12 -0
- package/dist/visualization/viewer-interaction.js +123 -0
- package/dist/visualization/visualization.d.ts +26 -0
- package/dist/visualization/visualization.js +491 -0
- package/docs/ADR-001-standalone-baseline.md +16 -0
- package/docs/ADR-002-saved-evidence.md +21 -0
- package/docs/ADR-003-draft-overlays.md +23 -0
- package/docs/ADR-004-rdf-query-path.md +37 -0
- package/docs/ADR-005-governance-and-optional-oml.md +37 -0
- package/docs/ADR-006-provider-boundaries.md +15 -0
- package/docs/ADR-007-toolkit-modules.md +53 -0
- package/docs/ADR-008-weftlet-packages.md +32 -0
- package/docs/CLOUD-ENVIRONMENT.md +56 -0
- package/docs/ENVIRONMENT-VALIDATION.md +78 -0
- package/docs/MIGRATING-TO-WEFTLET.md +54 -0
- package/docs/PROVIDERS.md +121 -0
- package/docs/RELEASING.md +144 -0
- package/docs/ROADMAP.md +53 -0
- package/docs/SOURCE-INVENTORY.md +48 -0
- package/docs/VALIDATION.md +490 -0
- package/docs/VIEWER.md +36 -0
- package/docs/architecture-baseline.json +57 -0
- package/docs/architecture-policy.md +41 -0
- package/docs/extraction-manifest.json +721 -0
- package/docs/licenses/REVIEW.md +16 -0
- package/docs/licenses/lru-cache-11.5.3-BlueOak.md +55 -0
- package/docs/licenses/negotiate-1.0.1-BSD.txt +22 -0
- package/docs/licenses/spark-md5-3.0.2-MIT.txt +7 -0
- package/examples/generic-typescript/context-graph.config.json +47 -0
- package/examples/generic-typescript/context-graph.oml.config.json +49 -0
- package/examples/generic-typescript/docs/search-capability.md +14 -0
- package/examples/generic-typescript/ontology/build.gradle +38 -0
- package/examples/generic-typescript/ontology/catalog.xml +5 -0
- package/examples/generic-typescript/ontology/settings.gradle +1 -0
- package/examples/generic-typescript/ontology/src/oml/context-graph.dev/core.oml +99 -0
- package/examples/generic-typescript/package.json +6 -0
- package/examples/generic-typescript/src/search.ts +11 -0
- package/examples/generic-typescript/test/search.test.ts +8 -0
- package/examples/generic-typescript/tsconfig.json +10 -0
- package/examples/pipeline/context-graph.config.json +49 -0
- package/examples/pipeline/docs/admission.md +6 -0
- package/examples/pipeline/docs/analysis.md +6 -0
- package/examples/pipeline/docs/discovery.md +6 -0
- package/examples/pipeline/docs/execution.md +6 -0
- package/examples/pipeline/docs/outcomes.md +6 -0
- package/examples/pipeline/docs/planning.md +6 -0
- package/examples/pipeline/package.json +6 -0
- package/examples/pipeline/src/admission.ts +9 -0
- package/examples/pipeline/src/analysis.ts +9 -0
- package/examples/pipeline/src/contracts/admission.ts +1 -0
- package/examples/pipeline/src/contracts/analysis.ts +1 -0
- package/examples/pipeline/src/contracts/discovery.ts +1 -0
- package/examples/pipeline/src/contracts/execution.ts +1 -0
- package/examples/pipeline/src/contracts/outcomes.ts +1 -0
- package/examples/pipeline/src/contracts/planning.ts +1 -0
- package/examples/pipeline/src/discovery.ts +9 -0
- package/examples/pipeline/src/execution.ts +9 -0
- package/examples/pipeline/src/outcomes.ts +9 -0
- package/examples/pipeline/src/planning.ts +9 -0
- package/examples/pipeline/src/queue.ts +12 -0
- package/examples/pipeline/test/pipeline.test.ts +17 -0
- package/examples/pipeline/tsconfig.json +12 -0
- package/package.json +116 -3
- package/runtime-check.d.mts +2 -0
- package/runtime-check.mjs +12 -0
- package/templates/context-graph-development/SKILL.md +37 -0
- package/third-party-tools.json +34 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
/** Materialize committed bytes without registering a worktree or touching the active checkout. */
|
|
8
|
+
export async function withCommittedTree(repositoryRoot, commitSha, operation) {
|
|
9
|
+
if (!/^[a-f0-9]{40}$/.test(commitSha))
|
|
10
|
+
throw new Error('Committed tree requires an exact SHA');
|
|
11
|
+
const temporary = await mkdtemp(join(tmpdir(), 'context-graph-commit-'));
|
|
12
|
+
try {
|
|
13
|
+
const { stdout: tree } = await execute('git', ['ls-tree', '-r', commitSha], {
|
|
14
|
+
cwd: repositoryRoot, maxBuffer: 64 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
// Do not dereference repository-controlled links outside the isolated tree.
|
|
17
|
+
if (/^(120000|160000) /m.test(tree)) {
|
|
18
|
+
throw new Error('Committed hydration does not yet support symlinks or submodules');
|
|
19
|
+
}
|
|
20
|
+
// Override checkout conversion for this command without changing the user's Git config.
|
|
21
|
+
const { stdout } = await execute('git', ['-c', 'core.autocrlf=false', 'archive', '--format=tar', commitSha], {
|
|
22
|
+
cwd: repositoryRoot, encoding: 'buffer', maxBuffer: 128 * 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
const archive = join(temporary, 'source.tar');
|
|
25
|
+
await writeFile(archive, stdout);
|
|
26
|
+
const { mkdir } = await import('node:fs/promises');
|
|
27
|
+
const root = join(temporary, 'checkout');
|
|
28
|
+
await mkdir(root);
|
|
29
|
+
await execute('tar', ['-xf', archive, '-C', root]);
|
|
30
|
+
return await operation(root);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await rm(temporary, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtractionContext, SemanticSource } from "@weftlet/core";
|
|
2
|
+
export interface GitDelta {
|
|
3
|
+
readonly changedPaths: readonly string[];
|
|
4
|
+
readonly deletedPaths: readonly string[];
|
|
5
|
+
readonly renamedPaths: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export interface GitTextMatch {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly line: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class GitRepository {
|
|
12
|
+
readonly root: string;
|
|
13
|
+
constructor(root: string);
|
|
14
|
+
private execute;
|
|
15
|
+
run(args: readonly string[]): Promise<string>;
|
|
16
|
+
resolveRef(ref: string): Promise<string>;
|
|
17
|
+
workingChanges(): Promise<readonly string[]>;
|
|
18
|
+
firstParent(commitSha: string): Promise<string | undefined>;
|
|
19
|
+
requireExactCleanCommit(commitSha: string): Promise<void>;
|
|
20
|
+
listFilesAt(commitSha: string, roots: readonly string[]): Promise<readonly string[]>;
|
|
21
|
+
listAllFilesAt(commitSha: string): Promise<readonly string[]>;
|
|
22
|
+
readFileAt(commitSha: string, path: string): Promise<string>;
|
|
23
|
+
findTextAt(commitSha: string, needles: readonly string[], roots: readonly string[]): Promise<readonly GitTextMatch[]>;
|
|
24
|
+
delta(baseSha: string | undefined, headSha: string, roots: readonly string[]): Promise<GitDelta>;
|
|
25
|
+
semanticSources(context: ExtractionContext, roots: readonly string[]): Promise<readonly SemanticSource[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { posix } from 'node:path';
|
|
5
|
+
import { sha256, slug } from "../infrastructure/util.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export class GitRepository {
|
|
8
|
+
root;
|
|
9
|
+
constructor(root) {
|
|
10
|
+
this.root = root;
|
|
11
|
+
}
|
|
12
|
+
async execute(args, trim) {
|
|
13
|
+
const { stdout } = await execFileAsync('git', [...args], {
|
|
14
|
+
cwd: this.root,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
return trim ? stdout.trim() : stdout;
|
|
20
|
+
}
|
|
21
|
+
run(args) {
|
|
22
|
+
return this.execute(args, true);
|
|
23
|
+
}
|
|
24
|
+
resolveRef(ref) {
|
|
25
|
+
return this.run(['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]);
|
|
26
|
+
}
|
|
27
|
+
async workingChanges() {
|
|
28
|
+
const prefix = await this.run(['rev-parse', '--show-prefix']);
|
|
29
|
+
const output = await this.execute(['status', '--porcelain=v1', '-z', '--untracked-files=all'], false);
|
|
30
|
+
const records = output.split('\0');
|
|
31
|
+
const paths = new Set();
|
|
32
|
+
for (let index = 0; index < records.length; index++) {
|
|
33
|
+
const record = records[index];
|
|
34
|
+
if (!record)
|
|
35
|
+
continue;
|
|
36
|
+
const path = posix.relative(prefix || '.', record.slice(3));
|
|
37
|
+
if (!path.startsWith('.context-graph/'))
|
|
38
|
+
paths.add(path);
|
|
39
|
+
if (/[RC]/.test(record.slice(0, 2))) {
|
|
40
|
+
const prior = records[++index];
|
|
41
|
+
if (prior)
|
|
42
|
+
paths.add(posix.relative(prefix || '.', prior));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...paths].sort();
|
|
46
|
+
}
|
|
47
|
+
async firstParent(commitSha) {
|
|
48
|
+
const parents = (await this.run(['show', '-s', '--format=%P', commitSha]))
|
|
49
|
+
.split(/\s+/)
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
return parents[0];
|
|
52
|
+
}
|
|
53
|
+
async requireExactCleanCommit(commitSha) {
|
|
54
|
+
const head = await this.resolveRef('HEAD');
|
|
55
|
+
if (head !== commitSha) {
|
|
56
|
+
throw new Error(`Ref resolved to ${commitSha}, but checked-out HEAD is ${head}`);
|
|
57
|
+
}
|
|
58
|
+
const status = await this.run(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
59
|
+
const relevant = status
|
|
60
|
+
.split('\n')
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.filter((line) => !line.includes('.context-graph/'));
|
|
63
|
+
if (relevant.length > 0) {
|
|
64
|
+
throw new Error('Committed hydration requires a clean worktree. Preserve and commit changes through normal review, then retry. Dirty-state overlays are not implemented.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async listFilesAt(commitSha, roots) {
|
|
68
|
+
const output = await this.run(['ls-tree', '-r', '--name-only', commitSha, '--', ...roots]);
|
|
69
|
+
return output.split('\n').filter(Boolean).sort();
|
|
70
|
+
}
|
|
71
|
+
listAllFilesAt(commitSha) {
|
|
72
|
+
return this.listFilesAt(commitSha, ['.']);
|
|
73
|
+
}
|
|
74
|
+
readFileAt(commitSha, path) {
|
|
75
|
+
// Git object paths are top-level-relative unless explicitly prefixed with ./.
|
|
76
|
+
return this.execute(['show', `${commitSha}:./${path}`], false);
|
|
77
|
+
}
|
|
78
|
+
async findTextAt(commitSha, needles, roots) {
|
|
79
|
+
if (needles.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
let output = '';
|
|
82
|
+
try {
|
|
83
|
+
output = await this.run([
|
|
84
|
+
'grep',
|
|
85
|
+
'-n',
|
|
86
|
+
'-F',
|
|
87
|
+
...needles.flatMap((needle) => ['-e', needle]),
|
|
88
|
+
commitSha,
|
|
89
|
+
'--',
|
|
90
|
+
...roots,
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code === 1)
|
|
95
|
+
return [];
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
const matches = new Map();
|
|
99
|
+
for (const line of output.split('\n').filter(Boolean)) {
|
|
100
|
+
const parsed = line.match(/^[^:]+:(.*?):(\d+):/);
|
|
101
|
+
if (!parsed?.[1] || !parsed[2])
|
|
102
|
+
continue;
|
|
103
|
+
const match = { path: parsed[1], line: Number(parsed[2]) };
|
|
104
|
+
if (!matches.has(match.path))
|
|
105
|
+
matches.set(match.path, match);
|
|
106
|
+
}
|
|
107
|
+
return [...matches.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
108
|
+
}
|
|
109
|
+
async delta(baseSha, headSha, roots) {
|
|
110
|
+
if (!baseSha) {
|
|
111
|
+
return {
|
|
112
|
+
changedPaths: await this.listFilesAt(headSha, roots),
|
|
113
|
+
deletedPaths: [],
|
|
114
|
+
renamedPaths: {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const output = await this.run([
|
|
118
|
+
'diff',
|
|
119
|
+
'--relative',
|
|
120
|
+
'--name-status',
|
|
121
|
+
'--find-renames=50%',
|
|
122
|
+
baseSha,
|
|
123
|
+
headSha,
|
|
124
|
+
'--',
|
|
125
|
+
...roots,
|
|
126
|
+
]);
|
|
127
|
+
const changed = new Set();
|
|
128
|
+
const deleted = new Set();
|
|
129
|
+
const renamed = {};
|
|
130
|
+
for (const line of output.split('\n').filter(Boolean)) {
|
|
131
|
+
const [status = '', first = '', second = ''] = line.split('\t');
|
|
132
|
+
if (status.startsWith('R')) {
|
|
133
|
+
renamed[first] = second;
|
|
134
|
+
changed.add(second);
|
|
135
|
+
deleted.add(first);
|
|
136
|
+
}
|
|
137
|
+
else if (status === 'D') {
|
|
138
|
+
deleted.add(first);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
changed.add(first);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
changedPaths: [...changed].sort(),
|
|
146
|
+
deletedPaths: [...deleted].sort(),
|
|
147
|
+
renamedPaths: renamed,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async semanticSources(context, roots) {
|
|
151
|
+
const paths = (await this.listFilesAt(context.commitSha, roots)).filter((path) => /\.(md|mdx|txt)$/i.test(path));
|
|
152
|
+
return Promise.all(paths.map(async (path) => {
|
|
153
|
+
const content = await this.readFileAt(context.commitSha, path);
|
|
154
|
+
const explicit = content.match(/^semantic-id:\s*([^\s]+)\s*$/m)?.[1];
|
|
155
|
+
return {
|
|
156
|
+
stableSemanticId: explicit ?? `doc:${slug(path)}`,
|
|
157
|
+
path,
|
|
158
|
+
content,
|
|
159
|
+
digest: sha256(content),
|
|
160
|
+
};
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function normalizePath(value) {
|
|
2
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
3
|
+
}
|
|
4
|
+
function escapeRegularExpression(character) {
|
|
5
|
+
return /[\\^$.*+?()[\]{}|]/.test(character) ? `\\${character}` : character;
|
|
6
|
+
}
|
|
7
|
+
export function matchesRepositoryGlob(path, pattern) {
|
|
8
|
+
const normalizedPath = normalizePath(path);
|
|
9
|
+
const normalizedPattern = normalizePath(pattern);
|
|
10
|
+
let expression = '^';
|
|
11
|
+
for (let index = 0; index < normalizedPattern.length; index += 1) {
|
|
12
|
+
const character = normalizedPattern[index];
|
|
13
|
+
if (character === '*' && normalizedPattern[index + 1] === '*') {
|
|
14
|
+
index += 1;
|
|
15
|
+
if (normalizedPattern[index + 1] === '/') {
|
|
16
|
+
index += 1;
|
|
17
|
+
expression += '(?:.*/)?';
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
expression += '.*';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (character === '*') {
|
|
24
|
+
expression += '[^/]*';
|
|
25
|
+
}
|
|
26
|
+
else if (character === '?') {
|
|
27
|
+
expression += '[^/]';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
expression += escapeRegularExpression(character);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`${expression}$`).test(normalizedPath);
|
|
34
|
+
}
|
|
35
|
+
export function contractPatternRoots(patterns) {
|
|
36
|
+
return [
|
|
37
|
+
...new Set(patterns.map((pattern) => {
|
|
38
|
+
const normalized = normalizePath(pattern);
|
|
39
|
+
const wildcard = normalized.search(/[?*]/);
|
|
40
|
+
if (wildcard < 0)
|
|
41
|
+
return normalized;
|
|
42
|
+
const slash = normalized.lastIndexOf('/', wildcard);
|
|
43
|
+
return slash < 0 ? '.' : normalized.slice(0, slash);
|
|
44
|
+
})),
|
|
45
|
+
].sort();
|
|
46
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function inWorkingScope(path: string, patterns: readonly string[]): boolean;
|
|
2
|
+
/** The same bounded bytes are used when capturing drafts and checking their freshness. */
|
|
3
|
+
export declare function readWorkingContent(root: string, path: string): Promise<Buffer | null>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { matchesRepositoryGlob } from "./glob.js";
|
|
4
|
+
export function inWorkingScope(path, patterns) {
|
|
5
|
+
const normalize = (value) => value.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '').replace(/\/$/, '') || '.';
|
|
6
|
+
const normalizedPath = normalize(path);
|
|
7
|
+
if (normalizedPath === '..' || normalizedPath.startsWith('../'))
|
|
8
|
+
return false;
|
|
9
|
+
return patterns.map(normalize).some((pattern) => pattern === '.' || matchesRepositoryGlob(normalizedPath, pattern) ||
|
|
10
|
+
(!/[*?]/.test(pattern) && normalizedPath.startsWith(`${pattern}/`)));
|
|
11
|
+
}
|
|
12
|
+
/** The same bounded bytes are used when capturing drafts and checking their freshness. */
|
|
13
|
+
export async function readWorkingContent(root, path) {
|
|
14
|
+
const absolute = resolve(root, path);
|
|
15
|
+
const outside = (value) => value === '..' || /^\.\.[\\/]/.test(value) || isAbsolute(value);
|
|
16
|
+
if (outside(relative(root, absolute)) || /(?:^|\/)\.env(?:\.|$)/.test(path)) {
|
|
17
|
+
throw new Error(`Unsafe overlay path: ${path}`);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const info = await lstat(absolute);
|
|
21
|
+
if (outside(relative(await realpath(root), await realpath(absolute))))
|
|
22
|
+
throw new Error('Overlay path escapes checkout');
|
|
23
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
24
|
+
throw new Error(`Overlay requires a regular file: ${path}`);
|
|
25
|
+
if (info.size > 1024 * 1024)
|
|
26
|
+
throw new Error(`Overlay source exceeds 1 MiB: ${path}`);
|
|
27
|
+
return await readFile(absolute);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'ENOENT')
|
|
31
|
+
throw error;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { rename } from 'node:fs/promises';
|
|
2
|
+
import { platform } from 'node:process';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
/** Replace a complete file without unlinking the previous value, even under Windows contention. */
|
|
5
|
+
export async function atomicRename(source, destination) {
|
|
6
|
+
for (let retry = 0;; retry++) {
|
|
7
|
+
try {
|
|
8
|
+
await rename(source, destination);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const code = error.code;
|
|
13
|
+
// Windows readers, concurrent renames and scanners can temporarily deny replacement.
|
|
14
|
+
// Retry the atomic operation itself across processes; never delete/truncate the target.
|
|
15
|
+
// Bound waiting to 50 retries (4.55 seconds), then propagate the original I/O error.
|
|
16
|
+
if (platform !== 'win32' || !['EPERM', 'EACCES', 'EBUSY'].includes(code ?? '') || retry >= 50) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
await delay(Math.min((retry + 1) * 10, 100));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GraphSnapshot, SemanticArtifact, SnapshotStore } from "@weftlet/core";
|
|
2
|
+
export declare class FileSnapshotStore implements SnapshotStore {
|
|
3
|
+
private readonly root;
|
|
4
|
+
constructor(root: string);
|
|
5
|
+
write(snapshot: GraphSnapshot): Promise<void>;
|
|
6
|
+
read(snapshotId: string): Promise<GraphSnapshot | undefined>;
|
|
7
|
+
readHead(headName: string): Promise<GraphSnapshot | undefined>;
|
|
8
|
+
updateHead(headName: string, snapshotId: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare class SemanticArtifactStore {
|
|
11
|
+
private readonly root;
|
|
12
|
+
constructor(root: string);
|
|
13
|
+
read(key: string): Promise<SemanticArtifact | undefined>;
|
|
14
|
+
write(artifact: SemanticArtifact): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Modified for standalone extraction; see docs/extraction-manifest.json.
|
|
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';
|
|
5
|
+
import { atomicRename } from './atomic-rename.js';
|
|
6
|
+
async function readJson(path) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT')
|
|
12
|
+
return undefined;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function atomicJson(path, value) {
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
19
|
+
try {
|
|
20
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
21
|
+
await atomicRename(temporary, path);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await rm(temporary, { force: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function safeKey(key) {
|
|
28
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/.test(key))
|
|
29
|
+
throw new Error('Unsafe storage identifier');
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
function snapshotPath(root, snapshotId) {
|
|
33
|
+
if (snapshotId.length > 256 ||
|
|
34
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(snapshotId) ||
|
|
35
|
+
snapshotId === '.' ||
|
|
36
|
+
snapshotId === '..') {
|
|
37
|
+
throw new Error('snapshotId must be a safe storage identifier');
|
|
38
|
+
}
|
|
39
|
+
const directory = resolve(root, 'snapshots');
|
|
40
|
+
const candidate = resolve(directory, `${snapshotId}.json`);
|
|
41
|
+
const pathFromDirectory = relative(directory, candidate);
|
|
42
|
+
if (pathFromDirectory === '..' ||
|
|
43
|
+
pathFromDirectory.startsWith(`..${sep}`) ||
|
|
44
|
+
isAbsolute(pathFromDirectory)) {
|
|
45
|
+
throw new Error('snapshotId must resolve within the snapshot directory');
|
|
46
|
+
}
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
export class FileSnapshotStore {
|
|
50
|
+
root;
|
|
51
|
+
constructor(root) {
|
|
52
|
+
this.root = root;
|
|
53
|
+
}
|
|
54
|
+
async write(snapshot) {
|
|
55
|
+
const destination = snapshotPath(this.root, snapshot.manifest.snapshotId);
|
|
56
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
57
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
await writeFile(temporary, JSON.stringify(snapshot), { flag: 'wx' });
|
|
60
|
+
// Linking a complete file publishes atomically without ever replacing a winner.
|
|
61
|
+
await link(temporary, destination);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== 'EEXIST')
|
|
65
|
+
throw error;
|
|
66
|
+
const existing = await this.read(snapshot.manifest.snapshotId);
|
|
67
|
+
if (existing?.manifest.contentDigest !== snapshot.manifest.contentDigest) {
|
|
68
|
+
throw new Error(`Refusing to overwrite immutable snapshot ${snapshot.manifest.snapshotId} with different content`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await rm(temporary, { force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
read(snapshotId) {
|
|
76
|
+
return readJson(snapshotPath(this.root, snapshotId));
|
|
77
|
+
}
|
|
78
|
+
async readHead(headName) {
|
|
79
|
+
const head = await readJson(join(this.root, 'heads', `${safeKey(headName)}.json`));
|
|
80
|
+
return head ? this.read(head.snapshotId) : undefined;
|
|
81
|
+
}
|
|
82
|
+
async updateHead(headName, snapshotId) {
|
|
83
|
+
if (!(await this.read(snapshotId)))
|
|
84
|
+
throw new Error('Cannot point a head to an absent snapshot');
|
|
85
|
+
return atomicJson(join(this.root, 'heads', `${safeKey(headName)}.json`), { snapshotId });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export class SemanticArtifactStore {
|
|
89
|
+
root;
|
|
90
|
+
constructor(root) {
|
|
91
|
+
this.root = root;
|
|
92
|
+
}
|
|
93
|
+
read(key) {
|
|
94
|
+
return readJson(join(this.root, 'artifacts', 'semantic', `${safeKey(key)}.json`));
|
|
95
|
+
}
|
|
96
|
+
async write(artifact) {
|
|
97
|
+
const path = join(this.root, 'artifacts', 'semantic', `${safeKey(artifact.key)}.json`);
|
|
98
|
+
await mkdir(dirname(path), { recursive: true });
|
|
99
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
100
|
+
try {
|
|
101
|
+
await writeFile(temporary, JSON.stringify(artifact), { flag: 'wx' });
|
|
102
|
+
await link(temporary, path);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code !== 'EEXIST')
|
|
106
|
+
throw error;
|
|
107
|
+
if ((await this.read(artifact.key))?.outputDigest !== artifact.outputDigest) {
|
|
108
|
+
throw new Error('Concurrent semantic artifact differs; retry with explicit force refresh');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await rm(temporary, { force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './extraction/telemetry.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PathEdge {
|
|
2
|
+
id: string;
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
}
|
|
6
|
+
/** One shortest directed path over displayed facts only; it does not add inferred edges. */
|
|
7
|
+
export declare function findEvidencePath(edges: readonly PathEdge[], start: string, target: string, maxVisited?: number): {
|
|
8
|
+
found: boolean;
|
|
9
|
+
edgeIds: string[];
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function viewerScript(): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** One shortest directed path over displayed facts only; it does not add inferred edges. */
|
|
2
|
+
export function findEvidencePath(edges, start, target, maxVisited = 1000) {
|
|
3
|
+
const queue = [start];
|
|
4
|
+
const parents = new Map();
|
|
5
|
+
const visited = new Set([start]);
|
|
6
|
+
let truncated = false;
|
|
7
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
8
|
+
const current = queue[cursor];
|
|
9
|
+
if (current === target) {
|
|
10
|
+
const edgeIds = [];
|
|
11
|
+
let node = target;
|
|
12
|
+
while (parents.has(node)) {
|
|
13
|
+
const parent = parents.get(node);
|
|
14
|
+
edgeIds.unshift(parent.edge);
|
|
15
|
+
node = parent.node;
|
|
16
|
+
}
|
|
17
|
+
return { found: true, edgeIds, truncated };
|
|
18
|
+
}
|
|
19
|
+
for (const edge of edges.filter((item) => item.from === current)) {
|
|
20
|
+
if (visited.has(edge.to))
|
|
21
|
+
continue;
|
|
22
|
+
if (visited.size >= maxVisited) {
|
|
23
|
+
truncated = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
visited.add(edge.to);
|
|
27
|
+
parents.set(edge.to, { node: current, edge: edge.id });
|
|
28
|
+
queue.push(edge.to);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { found: false, edgeIds: [], truncated };
|
|
32
|
+
}
|
|
33
|
+
export function viewerScript() {
|
|
34
|
+
return `
|
|
35
|
+
(() => {
|
|
36
|
+
const findPath = ${findEvidencePath.toString()};
|
|
37
|
+
const model = JSON.parse(document.getElementById('visual-model').textContent);
|
|
38
|
+
const search = document.getElementById('graph-search');
|
|
39
|
+
const cluster = document.getElementById('graph-cluster');
|
|
40
|
+
const detail = document.getElementById('node-detail');
|
|
41
|
+
const target = document.getElementById('path-target');
|
|
42
|
+
const status = document.getElementById('path-status');
|
|
43
|
+
const layers = [...document.querySelectorAll('[data-layer]')];
|
|
44
|
+
const edges = [...document.querySelectorAll('[data-visual-edge]')];
|
|
45
|
+
const nodes = [...document.querySelectorAll('[data-visual-node]')];
|
|
46
|
+
const rows = [...document.querySelectorAll('[data-relationship-row]')];
|
|
47
|
+
let selected;
|
|
48
|
+
let pathIds = new Set();
|
|
49
|
+
const enabledEdges = () => {
|
|
50
|
+
const enabled = new Set(layers.filter(item => item.checked).map(item => item.dataset.layer));
|
|
51
|
+
return model.edges.filter(edge => enabled.has(edge.authority));
|
|
52
|
+
};
|
|
53
|
+
const update = () => {
|
|
54
|
+
const enabled = new Set(enabledEdges().map(edge => edge.id));
|
|
55
|
+
const query = search.value.trim().toLowerCase();
|
|
56
|
+
const clusterIds = new Set(model.nodes.filter(node => !cluster.value || node.cluster === cluster.value).map(node => node.id));
|
|
57
|
+
const connected = new Set();
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (const edge of edges) {
|
|
60
|
+
const visible = enabled.has(edge.dataset.edgeId) && (!query || edge.dataset.search.includes(query)) &&
|
|
61
|
+
(clusterIds.has(edge.dataset.from) || clusterIds.has(edge.dataset.to)) &&
|
|
62
|
+
(!document.getElementById('neighbors-only').checked || !selected ||
|
|
63
|
+
edge.dataset.from === selected || edge.dataset.to === selected);
|
|
64
|
+
edge.classList.toggle('is-hidden', !visible);
|
|
65
|
+
edge.classList.toggle('is-highlighted', pathIds.has(edge.dataset.edgeId));
|
|
66
|
+
if (visible) { count++; connected.add(edge.dataset.from); connected.add(edge.dataset.to); }
|
|
67
|
+
}
|
|
68
|
+
for (const node of nodes) {
|
|
69
|
+
node.classList.toggle('is-hidden', !connected.has(node.dataset.nodeId) && node.dataset.nodeId !== selected);
|
|
70
|
+
node.classList.toggle('is-selected', node.dataset.nodeId === selected);
|
|
71
|
+
}
|
|
72
|
+
const visibleIds = new Set(edges.filter(edge => !edge.classList.contains('is-hidden')).map(edge => edge.dataset.edgeId));
|
|
73
|
+
rows.forEach(row => row.classList.toggle('is-hidden', !visibleIds.has(row.dataset.edgeId)));
|
|
74
|
+
document.getElementById('visible-count').textContent = count + ' visible relationships from this page';
|
|
75
|
+
};
|
|
76
|
+
const select = (id) => {
|
|
77
|
+
selected = id; pathIds.clear(); status.textContent = '';
|
|
78
|
+
detail.replaceChildren();
|
|
79
|
+
const node = model.nodes.find(node => node.id === id);
|
|
80
|
+
const heading = document.createElement('strong');
|
|
81
|
+
heading.textContent = node.label + ' · ' + node.kind;
|
|
82
|
+
detail.append(heading);
|
|
83
|
+
const identifier = document.createElement('p'); identifier.textContent = id; detail.append(identifier);
|
|
84
|
+
for (const edge of model.edges.filter(edge => edge.from === id || edge.to === id)) {
|
|
85
|
+
const item = document.createElement('p');
|
|
86
|
+
item.textContent = edge.authority + ': ' + edge.from + ' → ' + edge.label + ' → ' + edge.to + '\\n' + edge.detail;
|
|
87
|
+
const source = document.createElement(edge.sourceUrl ? 'a' : 'code');
|
|
88
|
+
source.textContent = edge.provenance + ' @ ' + edge.source.commitSha.slice(0,12);
|
|
89
|
+
if (edge.sourceUrl) { source.href = edge.sourceUrl; source.target = '_blank'; source.rel = 'noopener noreferrer'; }
|
|
90
|
+
item.append(document.createElement('br'), source);
|
|
91
|
+
const focus = document.createElement('button'); focus.textContent = 'Show source relationships';
|
|
92
|
+
focus.addEventListener('click', () => { search.value = edge.source.path; update(); });
|
|
93
|
+
item.append(document.createElement('br'), focus); detail.append(item);
|
|
94
|
+
}
|
|
95
|
+
update();
|
|
96
|
+
};
|
|
97
|
+
nodes.forEach(node => {
|
|
98
|
+
node.addEventListener('click', () => select(node.dataset.nodeId));
|
|
99
|
+
node.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); select(node.dataset.nodeId); } });
|
|
100
|
+
});
|
|
101
|
+
document.getElementById('highlight-path').addEventListener('click', () => {
|
|
102
|
+
if (!selected) { status.textContent = 'Select a start node first.'; return; }
|
|
103
|
+
const result = findPath(enabledEdges(), selected, target.value);
|
|
104
|
+
pathIds = new Set(result.edgeIds);
|
|
105
|
+
status.textContent = result.found ? 'One shortest directed path: ' + result.edgeIds.length + ' relationships.' :
|
|
106
|
+
'No path found in this displayed page and authority selection. Check coverage and continuation.';
|
|
107
|
+
if (result.truncated) status.textContent += ' Exploration limit reached.';
|
|
108
|
+
search.value = ''; cluster.value = ''; document.getElementById('neighbors-only').checked = false;
|
|
109
|
+
update();
|
|
110
|
+
});
|
|
111
|
+
search.addEventListener('input', update); cluster.addEventListener('change', update);
|
|
112
|
+
document.getElementById('neighbors-only').addEventListener('change', update);
|
|
113
|
+
layers.forEach(item => item.addEventListener('change', () => { pathIds.clear(); status.textContent = ''; update(); }));
|
|
114
|
+
document.getElementById('reset-filters').addEventListener('click', () => {
|
|
115
|
+
search.value = ''; cluster.value = ''; selected = undefined; pathIds.clear();
|
|
116
|
+
document.getElementById('neighbors-only').checked = false;
|
|
117
|
+
layers.forEach(item => { item.checked = true; });
|
|
118
|
+
detail.textContent = 'Select a node to inspect its cited relationships.'; status.textContent = ''; update();
|
|
119
|
+
});
|
|
120
|
+
update();
|
|
121
|
+
})();
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ContextPack, ImpactQueryService, ImpactScanRequest, GraphFact } from "@weftlet/core";
|
|
2
|
+
export type ContextGraphVisualizationFormat = 'dot' | 'html' | 'svg';
|
|
3
|
+
export interface VisualizationWriteOptions {
|
|
4
|
+
readonly outputPath: string;
|
|
5
|
+
readonly format?: ContextGraphVisualizationFormat;
|
|
6
|
+
readonly force?: boolean;
|
|
7
|
+
readonly sourceUrlTemplate?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface VisualizationWriteResult {
|
|
10
|
+
readonly outputPath: string;
|
|
11
|
+
readonly format: ContextGraphVisualizationFormat;
|
|
12
|
+
readonly snapshotId: string;
|
|
13
|
+
readonly commitSha: string;
|
|
14
|
+
readonly factCount: number;
|
|
15
|
+
readonly runtimeEvidenceCount: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const MAX_VISUALIZATION_FACTS = 200;
|
|
18
|
+
export declare const MAX_VISUALIZATION_RUNTIME_REFERENCES = 500;
|
|
19
|
+
export declare function sourceLink(source: GraphFact['provenance'], template?: string): string | undefined;
|
|
20
|
+
export declare function renderContextPackDot(pack: ContextPack): string;
|
|
21
|
+
export declare function renderContextPackSvg(pack: ContextPack): string;
|
|
22
|
+
export declare function renderContextPackHtml(pack: ContextPack, sourceUrlTemplate?: string): string;
|
|
23
|
+
export declare function resolveVisualizationFormat(outputPath: string, format?: string): ContextGraphVisualizationFormat;
|
|
24
|
+
export declare function renderContextPackVisualization(pack: ContextPack, format: ContextGraphVisualizationFormat, sourceUrlTemplate?: string): string;
|
|
25
|
+
export declare function writeContextPackVisualization(pack: ContextPack, options: VisualizationWriteOptions): Promise<VisualizationWriteResult>;
|
|
26
|
+
export declare function visualizeContextPack(queryService: ImpactQueryService, request: ImpactScanRequest, options: VisualizationWriteOptions): Promise<VisualizationWriteResult>;
|