@hraness/wordcell 0.20.0-bootstrap.1 → 0.21.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/README.md +29 -15
- package/dist/agent-context.js +1 -0
- package/dist/agent-guide-audit.js +1 -0
- package/dist/attachments.js +1 -0
- package/dist/authoring.js +1 -0
- package/dist/authority-4sp9d552.js +663 -0
- package/dist/benchmark.js +1 -0
- package/dist/capture.js +1 -0
- package/dist/cli.js +3781 -32
- package/dist/clip/acquire.js +1 -0
- package/dist/clip/args.js +1 -0
- package/dist/clip/bounded-byte-buffer.js +1 -0
- package/dist/clip/browser-profiles.js +1 -0
- package/dist/clip/bundle-reader.js +1 -0
- package/dist/clip/cli.js +1 -0
- package/dist/clip/cookies.js +1 -0
- package/dist/clip/doctor.js +1 -0
- package/dist/clip/jobs.js +1 -0
- package/dist/clip/network-proxy.js +1 -0
- package/dist/clip/network.js +1 -0
- package/dist/clip/persist.js +1 -0
- package/dist/clip/refresh.js +1 -0
- package/dist/clip/terminal.js +1 -0
- package/dist/evaluation-builder.js +13 -5
- package/dist/evaluation-kb.js +11 -4
- package/dist/evaluation.js +1 -0
- package/dist/git.js +1 -0
- package/dist/graph-authority.js +29 -0
- package/dist/graph-percolation.js +16 -0
- package/dist/graph.js +1 -0
- package/dist/index-0k2x4nn9.js +393 -0
- package/dist/{index-n05s3wsb.js → index-115b07ap.js} +156 -541
- package/dist/index-11621h23.js +160 -0
- package/dist/{index-tkpxp48t.js → index-51bap0mq.js} +2 -2
- package/dist/{index-8a08v6x8.js → index-59ssmqqm.js} +1 -1
- package/dist/index-bcfn9xah.js +472 -0
- package/dist/index-bgfzwt4h.js +51 -0
- package/dist/{index-bs0yf4pp.js → index-pbr6rv47.js} +2 -2
- package/dist/{index-m7et4rz4.js → index-vdw5yjfa.js} +61 -9
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +61 -34
- package/dist/navigation.js +1 -0
- package/dist/pdf.js +1 -0
- package/dist/percolate.js +1 -0
- package/dist/portfolio.js +11 -4
- package/dist/query.js +1 -0
- package/dist/repository-memory.js +1 -0
- package/dist/sdk.js +10 -3
- package/dist/search-rules.js +1 -0
- package/dist/search.js +1 -0
- package/dist/semantic.js +3 -1
- package/dist/source-inbox.js +1 -0
- package/dist/untrusted-content.js +1 -0
- package/dist/url-intelligence.js +1 -0
- package/dist/workflow.js +1 -0
- package/dist/workflows/decision-context.js +11 -4
- package/dist/workflows/explain-change.js +1 -0
- package/dist/workflows/index.js +11 -4
- package/dist/workflows/plan-radar.js +1 -0
- package/package.json +24 -6
- package/skills/wordcell/SKILL.md +2 -2
- package/skills/wordcell/references/percolate.md +4 -0
- package/skills/wordcell/references/query.md +6 -0
- package/src/cli-program.ts +45 -2
- package/src/graph-authority-model.ts +116 -0
- package/src/graph-authority.ts +219 -0
- package/src/graph-cache-filesystem.ts +4 -0
- package/src/graph-cli.ts +96 -0
- package/src/graph-facts.ts +206 -0
- package/src/graph-percolation.ts +39 -0
- package/src/graph-query.ts +122 -0
- package/src/index.ts +2 -0
- package/src/oh/authority.ts +193 -0
- package/src/oh/programs.ts +99 -0
- package/src/oh/schema.ts +40 -0
- package/src/oh/snapshot.ts +114 -0
- package/src/oh/validation.ts +61 -0
- package/src/sdk.ts +51 -6
- package/dist/index-sy0zety7.js +0 -3630
- package/dist/kb-alias.js +0 -55
- package/src/kb-alias.ts +0 -14
package/src/graph-cli.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { queryGraph, rebuildGraph, verifyGraph } from "./graph-authority.js";
|
|
2
|
+
import type { GraphQueryRequest, GraphQueryResult, GraphVerification } from "./graph-authority-model.js";
|
|
3
|
+
import { validateGraphQueryRequest } from "./graph-query.js";
|
|
4
|
+
|
|
5
|
+
type GraphCliBase = Readonly<{ root: string; index?: string; json: boolean }>;
|
|
6
|
+
export type GraphCliCommand =
|
|
7
|
+
| GraphCliBase & Readonly<{ kind: "graph-rebuild"; fresh: boolean }>
|
|
8
|
+
| GraphCliBase & Readonly<{ kind: "graph-verify" }>
|
|
9
|
+
| GraphCliBase & Readonly<{ kind: "graph-query"; request: GraphQueryRequest; persisted: boolean }>;
|
|
10
|
+
export type GraphCliParseResult =
|
|
11
|
+
| Readonly<{ ok: true; value: GraphCliCommand }>
|
|
12
|
+
| Readonly<{ ok: false; message: string }>;
|
|
13
|
+
export type GraphCliDependencies = Readonly<{
|
|
14
|
+
queryGraph?: typeof queryGraph;
|
|
15
|
+
rebuildGraph?: typeof rebuildGraph;
|
|
16
|
+
verifyGraph?: typeof verifyGraph;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export function parseGraphCommand(arguments_: readonly string[]): GraphCliParseResult {
|
|
20
|
+
const action = arguments_[0];
|
|
21
|
+
if (action !== "rebuild" && action !== "verify" && action !== "query") {
|
|
22
|
+
return { ok: false, message: "unknown graph action" };
|
|
23
|
+
}
|
|
24
|
+
if (arguments_.length > 32) return { ok: false, message: "too many graph options" };
|
|
25
|
+
let root = ".";
|
|
26
|
+
let index: string | undefined;
|
|
27
|
+
let json = false;
|
|
28
|
+
let persisted = false;
|
|
29
|
+
let fresh = false;
|
|
30
|
+
const request: Record<string, unknown> = {};
|
|
31
|
+
const seen = new Set<string>();
|
|
32
|
+
for (let position = 1; position < arguments_.length; position += 1) {
|
|
33
|
+
const option = arguments_[position]!;
|
|
34
|
+
if (seen.has(option)) return { ok: false, message: "duplicate graph option" };
|
|
35
|
+
seen.add(option);
|
|
36
|
+
if (option === "--json") { json = true; continue; }
|
|
37
|
+
if (option === "--fresh" && action === "rebuild") { fresh = true; continue; }
|
|
38
|
+
if (option === "--persisted" && action === "query") { persisted = true; continue; }
|
|
39
|
+
if (option !== "--root" && option !== "--index"
|
|
40
|
+
&& !(action === "query" && ["--program", "--note", "--scope", "--predicate", "--depth", "--limit"].includes(option))) {
|
|
41
|
+
return { ok: false, message: "unknown graph option" };
|
|
42
|
+
}
|
|
43
|
+
const value = arguments_[++position];
|
|
44
|
+
if (value === undefined || value.startsWith("--") || value.length === 0 || Buffer.byteLength(value, "utf8") > 16_384) {
|
|
45
|
+
return { ok: false, message: "graph option requires a bounded value" };
|
|
46
|
+
}
|
|
47
|
+
if (option === "--root") { root = value; continue; }
|
|
48
|
+
if (option === "--index") { index = value; continue; }
|
|
49
|
+
if (option === "--depth" || option === "--limit") {
|
|
50
|
+
if (!/^[1-9][0-9]*$/u.test(value) || !Number.isSafeInteger(Number(value))) {
|
|
51
|
+
return { ok: false, message: "graph count must be a positive integer" };
|
|
52
|
+
}
|
|
53
|
+
if (option === "--depth") request.depth = Number(value);
|
|
54
|
+
else request.limits = { rows: Number(value) };
|
|
55
|
+
} else request[option.slice(2)] = value;
|
|
56
|
+
}
|
|
57
|
+
const base = { root, ...(index === undefined ? {} : { index }), json };
|
|
58
|
+
if (action === "rebuild") return { ok: true, value: { kind: "graph-rebuild", ...base, fresh } };
|
|
59
|
+
if (action === "verify") return { ok: true, value: { kind: "graph-verify", ...base } };
|
|
60
|
+
try {
|
|
61
|
+
return { ok: true, value: { kind: "graph-query", ...base, persisted, request: validateGraphQueryRequest(request) } };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { ok: false, message: error instanceof Error ? error.message : "invalid graph query" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function executeGraphCommand(
|
|
68
|
+
command: GraphCliCommand,
|
|
69
|
+
dependencies: GraphCliDependencies = {},
|
|
70
|
+
): Promise<Readonly<{ value: GraphQueryResult | GraphVerification; text: string; exitCode: number }>> {
|
|
71
|
+
const options = command.index === undefined ? {} : { index: command.index };
|
|
72
|
+
if (command.kind === "graph-query") {
|
|
73
|
+
const value = await (dependencies.queryGraph ?? queryGraph)(command.root, command.request, {
|
|
74
|
+
...options, persisted: command.persisted,
|
|
75
|
+
});
|
|
76
|
+
const lines = [
|
|
77
|
+
`${value.request.program}: ${value.rows.length} derived graph rows.`,
|
|
78
|
+
`Snapshot: ${value.revision}`,
|
|
79
|
+
"Proofs and exact source record digests are included in --json output.",
|
|
80
|
+
value.columns.join("\t"),
|
|
81
|
+
...value.rows.map((row) => row.values.map((atom) => JSON.stringify(atom)).join("\t")),
|
|
82
|
+
];
|
|
83
|
+
if (value.truncated || value.proofsTruncated) {
|
|
84
|
+
lines.push(`Partial result: ${value.truncationReasons.join(", ") || "proof evidence was truncated"}.`);
|
|
85
|
+
}
|
|
86
|
+
return { value, text: `${lines.join("\n")}\n`, exitCode: value.truncated || value.proofsTruncated ? 4 : 0 };
|
|
87
|
+
}
|
|
88
|
+
const value = command.kind === "graph-rebuild"
|
|
89
|
+
? await (dependencies.rebuildGraph ?? rebuildGraph)(command.root, { ...options, fresh: command.fresh })
|
|
90
|
+
: await (dependencies.verifyGraph ?? verifyGraph)(command.root, options);
|
|
91
|
+
return {
|
|
92
|
+
value,
|
|
93
|
+
text: `${command.kind === "graph-rebuild" ? "Rebuilt" : "Verified"} the local graph projection: ${value.records} notes, ${value.facts} facts.\nSnapshot: ${value.revision}\n`,
|
|
94
|
+
exitCode: 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
analyzeVault,
|
|
6
|
+
isCanonicalNoteId,
|
|
7
|
+
parseNote,
|
|
8
|
+
VaultAnalysisBudgetError,
|
|
9
|
+
type Note,
|
|
10
|
+
} from "./graph.js";
|
|
11
|
+
import {
|
|
12
|
+
GRAPH_LIMITS,
|
|
13
|
+
GraphAuthorityError,
|
|
14
|
+
type GraphAtom,
|
|
15
|
+
type GraphFact,
|
|
16
|
+
type GraphFactRelation,
|
|
17
|
+
type GraphSnapshot,
|
|
18
|
+
type GraphSourceRecord,
|
|
19
|
+
} from "./graph-authority-model.js";
|
|
20
|
+
import { documentIdState } from "./portfolio-identity.js";
|
|
21
|
+
import { analyzeAuthoredRepositoryScopes } from "./repository-memory.js";
|
|
22
|
+
import type { VaultSnapshot } from "./vault.js";
|
|
23
|
+
|
|
24
|
+
const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|
|
25
|
+
const sha256 = (value: string): string => createHash("sha256").update(value, "utf8").digest("hex");
|
|
26
|
+
|
|
27
|
+
/** Internal protocol serialization: only freshly constructed, finite JSON values enter here. */
|
|
28
|
+
function canonicalJson(value: unknown): string {
|
|
29
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
30
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
31
|
+
return `{${Object.entries(value).toSorted(([left], [right]) => compareText(left, right))
|
|
32
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function invalid(message: string): never {
|
|
36
|
+
throw new GraphAuthorityError("invalid-input", message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function budget(message: string): never {
|
|
40
|
+
throw new GraphAuthorityError("budget", message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function textAtom(value: unknown, label: string): string {
|
|
44
|
+
if (typeof value !== "string") invalid(`${label} must be a string.`);
|
|
45
|
+
if (Buffer.byteLength(value, "utf8") > GRAPH_LIMITS.atomBytes) {
|
|
46
|
+
budget(`${label} exceeds the ${GRAPH_LIMITS.atomBytes} byte graph atom limit.`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function markdownPath(value: unknown, label: string): string {
|
|
52
|
+
const path = textAtom(value, label);
|
|
53
|
+
if (!path.toLowerCase().endsWith(".md") || !isCanonicalNoteId(path.slice(0, -3))) {
|
|
54
|
+
invalid(`${label} must be a canonical vault-relative Markdown path.`);
|
|
55
|
+
}
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type CheckedSource = Readonly<{ note: Note; documentId: string | null; scopes: readonly string[] }>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive a bounded, immutable graph edition from authored Markdown.
|
|
63
|
+
* Caller-provided parsed fields and analysis are not evidence: facts are reparsed
|
|
64
|
+
* from the same bytes whose digests bind every returned source record.
|
|
65
|
+
*/
|
|
66
|
+
export function createGraphSnapshot(snapshot: VaultSnapshot): GraphSnapshot {
|
|
67
|
+
if (snapshot === null || typeof snapshot !== "object") invalid("A vault snapshot is required.");
|
|
68
|
+
const rootInput = textAtom(snapshot.root, "Vault root");
|
|
69
|
+
if (!isAbsolute(rootInput) || rootInput.includes("\0")) invalid("Vault root must be absolute.");
|
|
70
|
+
const root = resolve(rootInput);
|
|
71
|
+
const indexPath = textAtom(snapshot.indexPath, "Catalog path");
|
|
72
|
+
if (!isAbsolute(indexPath) || indexPath.includes("\0")) invalid("Catalog path must be absolute.");
|
|
73
|
+
const catalogPath = markdownPath(relative(root, resolve(indexPath)).split(sep).join("/"), "Catalog path");
|
|
74
|
+
const catalogNoteId = catalogPath.slice(0, -3);
|
|
75
|
+
if (!Array.isArray(snapshot.notes)) invalid("Vault notes must be an array.");
|
|
76
|
+
if (snapshot.notes.length > GRAPH_LIMITS.notes) {
|
|
77
|
+
budget(`Graph exceeds the ${GRAPH_LIMITS.notes} note limit.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let sourceBytes = 0;
|
|
81
|
+
const paths = new Set<string>();
|
|
82
|
+
// Bound all source bytes and identities before parsing or materializing facts.
|
|
83
|
+
const sources = snapshot.notes.map((value: unknown) => {
|
|
84
|
+
if (value === null || typeof value !== "object") invalid("Every vault note must be an object.");
|
|
85
|
+
const candidate = value as Record<string, unknown>;
|
|
86
|
+
const path = markdownPath(candidate.path, "Note path");
|
|
87
|
+
if (candidate.id !== path.slice(0, -3)) invalid(`Note ID must match its canonical path: ${path}.`);
|
|
88
|
+
if (paths.has(path)) invalid(`Duplicate note path: ${path}.`);
|
|
89
|
+
paths.add(path);
|
|
90
|
+
if (typeof candidate.content !== "string") invalid(`Note content must be a string: ${path}.`);
|
|
91
|
+
sourceBytes += Buffer.byteLength(candidate.content, "utf8");
|
|
92
|
+
if (sourceBytes > GRAPH_LIMITS.sourceBytes) {
|
|
93
|
+
budget(`Graph sources exceed the ${GRAPH_LIMITS.sourceBytes} byte limit.`);
|
|
94
|
+
}
|
|
95
|
+
return { path, content: candidate.content };
|
|
96
|
+
}).toSorted((left, right) => compareText(left.path, right.path));
|
|
97
|
+
|
|
98
|
+
const documentIds = new Set<string>();
|
|
99
|
+
const noteIds = new Set<string>();
|
|
100
|
+
let connectionObservations = 0;
|
|
101
|
+
const checked: CheckedSource[] = sources.map(({ path, content }) => {
|
|
102
|
+
let note: Note;
|
|
103
|
+
try {
|
|
104
|
+
note = parseNote(path, content);
|
|
105
|
+
} catch {
|
|
106
|
+
invalid(`Cannot parse authored Markdown: ${path}.`);
|
|
107
|
+
}
|
|
108
|
+
if (noteIds.has(note.id)) invalid(`Duplicate note ID: ${note.id}.`);
|
|
109
|
+
noteIds.add(note.id);
|
|
110
|
+
textAtom(note.title, `Title in ${path}`);
|
|
111
|
+
textAtom(note.properties.type ?? "", `Type in ${path}`);
|
|
112
|
+
for (const tag of note.tags) textAtom(tag, `Tag in ${path}`);
|
|
113
|
+
const identity = documentIdState(note.metadata);
|
|
114
|
+
if (identity.kind === "invalid") invalid(`Invalid document_id in ${path}.`);
|
|
115
|
+
const documentId = identity.kind === "valid" ? identity.documentId : null;
|
|
116
|
+
if (documentId !== null) {
|
|
117
|
+
if (documentIds.has(documentId)) invalid(`Duplicate document_id: ${documentId}.`);
|
|
118
|
+
documentIds.add(documentId);
|
|
119
|
+
}
|
|
120
|
+
const scopes = analyzeAuthoredRepositoryScopes(note.metadata);
|
|
121
|
+
if (!scopes.valid) invalid(`Invalid repository_scopes in ${path}.`);
|
|
122
|
+
for (const scope of scopes.scopes) textAtom(scope, `Repository scope in ${path}`);
|
|
123
|
+
connectionObservations += note.links.length
|
|
124
|
+
+ (note.relationDeclarations?.length ?? 0) + (note.relationIssues?.length ?? 0);
|
|
125
|
+
if (connectionObservations > GRAPH_LIMITS.facts) {
|
|
126
|
+
budget(`Graph connections exceed the ${GRAPH_LIMITS.facts} observation limit.`);
|
|
127
|
+
}
|
|
128
|
+
return { note, documentId, scopes: scopes.scopes };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let analysis: ReturnType<typeof analyzeVault>;
|
|
132
|
+
try {
|
|
133
|
+
analysis = analyzeVault(checked.map(({ note }) => note), {
|
|
134
|
+
catalogNoteId,
|
|
135
|
+
mentionScope: () => false,
|
|
136
|
+
maxNotes: GRAPH_LIMITS.notes,
|
|
137
|
+
maxConnectionObservations: GRAPH_LIMITS.facts,
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof VaultAnalysisBudgetError) budget(error.message);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const included = new Set(analysis.noteConnections.map(({ id }) => id));
|
|
145
|
+
const factGroups = new Map<string, Map<string, GraphFact>>();
|
|
146
|
+
let factCount = 0;
|
|
147
|
+
const add = (source: string, relation: GraphFactRelation, tuple: readonly GraphAtom[]): void => {
|
|
148
|
+
if (!included.has(source)) invalid(`Graph fact has no authored source: ${source}.`);
|
|
149
|
+
for (const atom of tuple) {
|
|
150
|
+
if (typeof atom === "string") textAtom(atom, relation);
|
|
151
|
+
else if (typeof atom === "number" && (!Number.isSafeInteger(atom) || atom < 1)) {
|
|
152
|
+
invalid(`Graph source lines must be positive safe integers: ${source}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const key = canonicalJson({ relation, tuple });
|
|
156
|
+
const group = factGroups.get(source) ?? new Map<string, GraphFact>();
|
|
157
|
+
if (group.has(key)) return;
|
|
158
|
+
if (factCount >= GRAPH_LIMITS.facts) budget(`Graph exceeds the ${GRAPH_LIMITS.facts} fact limit.`);
|
|
159
|
+
factCount += 1;
|
|
160
|
+
group.set(key, Object.freeze({ relation, tuple: Object.freeze([...tuple]) }));
|
|
161
|
+
factGroups.set(source, group);
|
|
162
|
+
};
|
|
163
|
+
for (const { note, scopes } of checked) {
|
|
164
|
+
if (!included.has(note.id)) continue;
|
|
165
|
+
const type = note.properties.type ?? "";
|
|
166
|
+
add(note.id, "wordcell.note", [note.id, note.path, note.title, type]);
|
|
167
|
+
if (type.normalize("NFC").toLocaleLowerCase("en-US") === "concept") {
|
|
168
|
+
add(note.id, "wordcell.concept", [note.id]);
|
|
169
|
+
}
|
|
170
|
+
for (const tag of note.tags) add(note.id, "wordcell.tag", [note.id, tag]);
|
|
171
|
+
for (const scope of scopes) add(note.id, "wordcell.scope", [note.id, scope]);
|
|
172
|
+
}
|
|
173
|
+
for (const link of analysis.contextualLinks) {
|
|
174
|
+
const source = link.source.slice(0, -3);
|
|
175
|
+
add(source, "wordcell.link", [source, link.target.slice(0, -3), link.line]);
|
|
176
|
+
}
|
|
177
|
+
for (const relation of analysis.authoredRelations) {
|
|
178
|
+
add(relation.source, "wordcell.relation", [
|
|
179
|
+
relation.source, relation.target, relation.predicate, relation.provenance.line,
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
for (const relation of analysis.externalAuthoredRelations) {
|
|
183
|
+
add(relation.source, "wordcell.external-relation", [
|
|
184
|
+
relation.source, relation.target, relation.predicate, relation.provenance.line,
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const sourceDigests = Object.freeze(sources.map(({ path, content }) =>
|
|
189
|
+
Object.freeze({ path, contentSha256: sha256(content) })));
|
|
190
|
+
const digests = new Map(sourceDigests.map(({ path, contentSha256 }) => [path, contentSha256]));
|
|
191
|
+
const records: readonly GraphSourceRecord[] = Object.freeze(checked
|
|
192
|
+
.filter(({ note }) => included.has(note.id))
|
|
193
|
+
.map(({ note, documentId }): GraphSourceRecord => Object.freeze({
|
|
194
|
+
key: `edition:note/${sha256(documentId === null ? `path:${note.id}` : `document:${documentId}`)}`,
|
|
195
|
+
id: note.id,
|
|
196
|
+
path: note.path,
|
|
197
|
+
documentId,
|
|
198
|
+
contentSha256: digests.get(note.path)!,
|
|
199
|
+
facts: Object.freeze([...(factGroups.get(note.id)?.entries() ?? [])]
|
|
200
|
+
.toSorted(([left], [right]) => compareText(left, right)).map(([, fact]) => fact)),
|
|
201
|
+
}))
|
|
202
|
+
.toSorted((left, right) => compareText(left.key, right.key)));
|
|
203
|
+
const identity = Object.freeze({ schemaVersion: 1 as const, vaultIdentity: sha256(root),
|
|
204
|
+
catalogNoteId, sourceDigests, records });
|
|
205
|
+
return Object.freeze({ ...identity, revision: sha256(canonicalJson(identity)), factCount });
|
|
206
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { analyzeVault, lookupNote, parseNote } from "./graph.js";
|
|
2
|
+
import { createGraphSnapshot, openGraphAuthority, GraphAuthorityError } from "./graph-authority.js";
|
|
3
|
+
import type { GraphQueryResult } from "./graph-authority.js";
|
|
4
|
+
import { validateGraphPercolationOptions } from "./graph-query.js";
|
|
5
|
+
import { percolateVault } from "./percolate.js";
|
|
6
|
+
import type { PercolateOptions, PercolationResultV2 } from "./percolate.js";
|
|
7
|
+
import type { VaultSnapshot } from "./vault.js";
|
|
8
|
+
|
|
9
|
+
export type GraphPercolationResult = Readonly<{
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
kind: "wordcell.graph-percolation";
|
|
12
|
+
revision: string;
|
|
13
|
+
suggestions: PercolationResultV2;
|
|
14
|
+
/** Positive shared-tag/concept support only. Absence, counts and predicate decisions remain host-owned. */
|
|
15
|
+
positiveSupport: Readonly<{ sharedTags: GraphQueryResult; sharedConcepts: GraphQueryResult }>;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/** Add bounded, separately inspectable positive proofs without changing percolation V2 candidates. */
|
|
19
|
+
export async function percolateWithGraph(snapshot: VaultSnapshot,
|
|
20
|
+
options: PercolateOptions & Readonly<{ note: string }>): Promise<GraphPercolationResult> {
|
|
21
|
+
options = validateGraphPercolationOptions(options);
|
|
22
|
+
const graph = createGraphSnapshot(snapshot);
|
|
23
|
+
const notes = snapshot.notes.map(note => parseNote(note.path, note.content));
|
|
24
|
+
const selected = lookupNote(notes, options.note);
|
|
25
|
+
if (selected.kind !== "found" || !graph.records.some(record => record.id === selected.note.id)) {
|
|
26
|
+
throw new GraphAuthorityError("invalid-input", "Proof-backed percolation requires one unambiguous content note.");
|
|
27
|
+
}
|
|
28
|
+
const analysis = analyzeVault(notes, { catalogNoteId: graph.catalogNoteId,
|
|
29
|
+
mentionScope: note => note.id === selected.note.id, maxMentionPairs: 250_000 });
|
|
30
|
+
const suggestions = percolateVault(notes, analysis, options);
|
|
31
|
+
const authority = await openGraphAuthority(snapshot);
|
|
32
|
+
try {
|
|
33
|
+
const limits = { rows: 1_000, workUnits: 1_000_000, resultBytes: 1024 * 1024 };
|
|
34
|
+
const sharedTags = await authority.query({ program: "shared-tags", note: selected.note.id, limits });
|
|
35
|
+
const sharedConcepts = await authority.query({ program: "shared-concepts", note: selected.note.id, limits });
|
|
36
|
+
return Object.freeze({ schemaVersion: 1, kind: "wordcell.graph-percolation", revision: graph.revision,
|
|
37
|
+
suggestions, positiveSupport: Object.freeze({ sharedTags, sharedConcepts }) });
|
|
38
|
+
} finally { await authority.close(); }
|
|
39
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isProxy } from "node:util/types";
|
|
2
|
+
import { isCanonicalNoteId, isCanonicalRelationPredicate } from "./graph.js";
|
|
3
|
+
import {
|
|
4
|
+
GRAPH_LIMITS,
|
|
5
|
+
GraphAuthorityError,
|
|
6
|
+
type GraphQueryLimits,
|
|
7
|
+
type GraphQueryRequest,
|
|
8
|
+
} from "./graph-authority-model.js";
|
|
9
|
+
import type { PercolateOptions } from "./percolate.js";
|
|
10
|
+
import { validateRepositoryScopeSelection } from "./repository-memory.js";
|
|
11
|
+
|
|
12
|
+
function invalid(message: string): never {
|
|
13
|
+
throw new GraphAuthorityError("invalid-input", message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function dataObject(value: unknown, label: string): Readonly<Record<string, unknown>> {
|
|
17
|
+
if (value === null || typeof value !== "object" || isProxy(value) || Array.isArray(value)) {
|
|
18
|
+
return invalid(`${label} must be a plain data object.`);
|
|
19
|
+
}
|
|
20
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
21
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
22
|
+
return invalid(`${label} must be a plain data object.`);
|
|
23
|
+
}
|
|
24
|
+
const keys = Reflect.ownKeys(value);
|
|
25
|
+
if (keys.length > 16) return invalid(`${label} contains too many fields.`);
|
|
26
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (typeof key !== "string") return invalid(`${label} must have only named data fields.`);
|
|
29
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
30
|
+
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
31
|
+
return invalid(`${label} must have only enumerable data fields.`);
|
|
32
|
+
}
|
|
33
|
+
result[key] = descriptor.value;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function boundedText(value: unknown, label: string): string {
|
|
39
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > GRAPH_LIMITS.atomBytes) {
|
|
40
|
+
return invalid(`${label} must be a bounded string.`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boundedInteger(value: unknown, maximum: number, label: string): number {
|
|
46
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
47
|
+
return invalid(`${label} must be an integer from 1 through ${maximum}.`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkedLimits(value: unknown): GraphQueryLimits {
|
|
53
|
+
const input = dataObject(value, "Graph query limits");
|
|
54
|
+
const result: Record<string, number> = {};
|
|
55
|
+
const keys = ["rows", "workUnits", "derivedTuples", "rounds", "proofDepth", "proofNodes",
|
|
56
|
+
"totalProofNodes", "resultBytes"] as const;
|
|
57
|
+
for (const key of Object.keys(input)) {
|
|
58
|
+
if (!(keys as readonly string[]).includes(key)) return invalid("Unknown graph query limit.");
|
|
59
|
+
}
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
if (Object.hasOwn(input, key)) {
|
|
62
|
+
result[key] = boundedInteger(input[key], GRAPH_LIMITS[key], `Graph query ${key}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze(result);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Validate and copy a named query before any scan, store open, or asynchronous work. */
|
|
69
|
+
export function validateGraphQueryRequest(value: unknown): GraphQueryRequest {
|
|
70
|
+
const input = dataObject(value, "Graph query");
|
|
71
|
+
const program = input.program;
|
|
72
|
+
if (program !== "backlinks" && program !== "reachability" && program !== "scope-route"
|
|
73
|
+
&& program !== "relation-closure" && program !== "shared-tags" && program !== "shared-concepts") {
|
|
74
|
+
return invalid("Graph query requires a supported named program.");
|
|
75
|
+
}
|
|
76
|
+
const allowed = new Set(["program", "limits", program === "scope-route" ? "scope" : "note"]);
|
|
77
|
+
if (program === "reachability" || program === "relation-closure") allowed.add("depth");
|
|
78
|
+
if (program === "relation-closure") allowed.add("predicate");
|
|
79
|
+
if (Object.keys(input).some((key) => !allowed.has(key))) {
|
|
80
|
+
return invalid("Graph query contains an option unsupported by its named program.");
|
|
81
|
+
}
|
|
82
|
+
const limits = Object.hasOwn(input, "limits") ? checkedLimits(input.limits) : undefined;
|
|
83
|
+
const extras = limits === undefined ? {} : { limits };
|
|
84
|
+
if (program === "scope-route") {
|
|
85
|
+
const scope = boundedText(input.scope, "Graph query scope");
|
|
86
|
+
try { validateRepositoryScopeSelection([scope]); } catch {
|
|
87
|
+
return invalid("Graph query scope must be an exact canonical repository path.");
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze({ program, scope, ...extras });
|
|
90
|
+
}
|
|
91
|
+
const note = boundedText(input.note, "Graph query note");
|
|
92
|
+
if (!isCanonicalNoteId(note)) return invalid("Graph query note must be an exact canonical note ID.");
|
|
93
|
+
const depth = Object.hasOwn(input, "depth")
|
|
94
|
+
? boundedInteger(input.depth, GRAPH_LIMITS.depth, "Graph query depth") : undefined;
|
|
95
|
+
const depthOption = depth === undefined ? {} : { depth };
|
|
96
|
+
if (program === "relation-closure") {
|
|
97
|
+
const predicate = boundedText(input.predicate, "Graph query predicate");
|
|
98
|
+
if (!isCanonicalRelationPredicate(predicate)) {
|
|
99
|
+
return invalid("Graph query predicate must be an exact lower-kebab relation predicate.");
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({ program, note, predicate, ...depthOption, ...extras });
|
|
102
|
+
}
|
|
103
|
+
if (program === "reachability") return Object.freeze({ program, note, ...depthOption, ...extras });
|
|
104
|
+
return Object.freeze({ program, note, ...extras });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Copy bounded advisory options without evaluating foreign getters. Note lookup remains Wordcell-owned. */
|
|
108
|
+
export function validateGraphPercolationOptions(value: unknown): PercolateOptions & Readonly<{ note: string }> {
|
|
109
|
+
const input = dataObject(value, "Graph percolation options");
|
|
110
|
+
if (Object.keys(input).some((key) => !["note", "minSupport", "limit"].includes(key))) {
|
|
111
|
+
return invalid("Unknown graph percolation option.");
|
|
112
|
+
}
|
|
113
|
+
const note = boundedText(input.note, "Graph percolation note");
|
|
114
|
+
if (note.trim() === "") return invalid("Graph percolation requires one note.");
|
|
115
|
+
const minSupport = Object.hasOwn(input, "minSupport")
|
|
116
|
+
? boundedInteger(input.minSupport, 1_000, "Graph percolation minSupport") : undefined;
|
|
117
|
+
if (minSupport === 1) return invalid("Graph percolation minSupport must be at least 2.");
|
|
118
|
+
const limit = Object.hasOwn(input, "limit")
|
|
119
|
+
? boundedInteger(input.limit, 1_000, "Graph percolation limit") : undefined;
|
|
120
|
+
return Object.freeze({ note, ...(minSupport === undefined ? {} : { minSupport }),
|
|
121
|
+
...(limit === undefined ? {} : { limit }) });
|
|
122
|
+
}
|