@nurix/nustack 0.23.20 → 0.23.22
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/dist/commands/context.js +78 -0
- package/dist/commands/gate.js +4 -0
- package/dist/index.js +5 -0
- package/dist/lib/context/compose.js +269 -0
- package/dist/lib/context/editCard.js +31 -0
- package/dist/lib/context/graph.js +107 -0
- package/dist/lib/context/hook.js +287 -0
- package/dist/lib/context/recall.js +26 -0
- package/dist/lib/context/register.js +211 -0
- package/dist/lib/context/render.js +103 -0
- package/dist/lib/context/resolve.js +104 -0
- package/dist/lib/context/writers.js +58 -0
- package/dist/lib/gate/readGate.js +9 -4
- package/dist/lib/gate/redirect.js +61 -0
- package/dist/lib/graph/projectRoot.js +39 -0
- package/dist/lib/local_store/paths.js +3 -0
- package/dist/program.js +14 -0
- package/package.json +3 -3
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
3
|
+
import { composePack, CONTEXT_EXIT_NO_REGISTER } from "../lib/context/compose.js";
|
|
4
|
+
import { openGraphForCommand, readCheckoutHead } from "../lib/context/graph.js";
|
|
5
|
+
import { docsRootOf, readRegister, REGISTER_SUBDIR } from "../lib/context/register.js";
|
|
6
|
+
import { CONTEXT_PACK_MAX_TOKENS, SECTION_NAMES } from "../lib/context/render.js";
|
|
7
|
+
import { resolveProjectRoot } from "../lib/graph/projectRoot.js";
|
|
8
|
+
export function diffPaths(diff) {
|
|
9
|
+
const paths = [];
|
|
10
|
+
for (const match of diff.matchAll(/^\+\+\+ (?:b\/)?(.+)$/gm)) {
|
|
11
|
+
const file = (match[1] ?? "").trim();
|
|
12
|
+
if (file !== "/dev/null" && !paths.includes(file))
|
|
13
|
+
paths.push(file);
|
|
14
|
+
}
|
|
15
|
+
return paths;
|
|
16
|
+
}
|
|
17
|
+
function readDiff(source) {
|
|
18
|
+
return readFileSync(source === "-" ? 0 : source, "utf8");
|
|
19
|
+
}
|
|
20
|
+
function jsonData(pack) {
|
|
21
|
+
const sections = {};
|
|
22
|
+
for (const name of SECTION_NAMES)
|
|
23
|
+
sections[name] = pack.rendered.sections[name].map((line) => ({ label: line.label, text: line.text }));
|
|
24
|
+
return { exitCode: pack.exitCode, objects: pack.objects, sections, dropped: pack.rendered.dropped, tokens: pack.rendered.tokens };
|
|
25
|
+
}
|
|
26
|
+
export async function runContext(textParts, options, cwd = process.cwd()) {
|
|
27
|
+
if (options.hook === true) {
|
|
28
|
+
const { runContextHook } = await import("../lib/context/hook.js");
|
|
29
|
+
if ((await runContextHook()) === "deadline")
|
|
30
|
+
process.exit(0);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const root = resolveProjectRoot(undefined, cwd);
|
|
34
|
+
const docsRoot = root === null ? "docs" : docsRootOf(root);
|
|
35
|
+
const register = root === null ? null : readRegister(root, docsRoot);
|
|
36
|
+
if (root === null || register === null) {
|
|
37
|
+
const where = `${docsRoot}/${REGISTER_SUBDIR.split("\\").join("/")}`;
|
|
38
|
+
const message = `no object register at ${where} — the context pack reads it; \`nustack graph\` answers structural questions without it`;
|
|
39
|
+
if (options.json === true) {
|
|
40
|
+
printJsonSuccess("context", { exitCode: CONTEXT_EXIT_NO_REGISTER, objects: [], sections: {}, dropped: [], tokens: 0, message });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.stderr.write(`${message}\n`);
|
|
44
|
+
}
|
|
45
|
+
process.exitCode = CONTEXT_EXIT_NO_REGISTER;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const budget = options.budget === undefined ? CONTEXT_PACK_MAX_TOKENS : Number(options.budget);
|
|
49
|
+
if (!Number.isInteger(budget) || budget <= 0)
|
|
50
|
+
throw new Error(`--budget must be a positive integer, got "${options.budget}"`);
|
|
51
|
+
const paths = [...(options.path ?? []), ...(options.diff === undefined ? [] : diffPaths(readDiff(options.diff)))];
|
|
52
|
+
const graph = await openGraphForCommand(root, {
|
|
53
|
+
noRefresh: options.refresh === false,
|
|
54
|
+
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const pack = composePack({
|
|
58
|
+
root,
|
|
59
|
+
cwd,
|
|
60
|
+
register,
|
|
61
|
+
text: textParts.join(" "),
|
|
62
|
+
paths,
|
|
63
|
+
graph: graph.view,
|
|
64
|
+
graphReason: graph.reason,
|
|
65
|
+
docsRoot,
|
|
66
|
+
checkoutHead: readCheckoutHead(root),
|
|
67
|
+
budgetTokens: budget,
|
|
68
|
+
});
|
|
69
|
+
if (options.json === true)
|
|
70
|
+
printJsonSuccess("context", jsonData(pack));
|
|
71
|
+
else
|
|
72
|
+
process.stdout.write(`${pack.rendered.text}\n`);
|
|
73
|
+
process.exitCode = pack.exitCode;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
graph.view?.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/commands/gate.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { hasManagedHook, installManagedHook, removeManagedHook } from "../lib/agent/claudeHooks.js";
|
|
4
4
|
import { readDesktopSettings } from "../lib/desktop_settings.js";
|
|
5
5
|
import { loadOutlineLibrary } from "../lib/gate/outline.js";
|
|
6
|
+
import { isRedirectOn } from "../lib/gate/redirect.js";
|
|
7
|
+
import { resolveProjectRoot } from "../lib/graph/projectRoot.js";
|
|
6
8
|
import { resolveThreshold } from "../lib/gate/settings.js";
|
|
7
9
|
import { printJsonSuccess } from "../lib/json_output.js";
|
|
8
10
|
import { readStoreVersion } from "../lib/local_store/database.js";
|
|
@@ -56,6 +58,7 @@ export async function readGateStatus() {
|
|
|
56
58
|
thresholdSource,
|
|
57
59
|
outline: library !== null && typeof library.outlineSource === "function" ? "tree-sitter" : "fallback",
|
|
58
60
|
storeVersion: await readStoreVersion(),
|
|
61
|
+
redirect: isRedirectOn(resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR, process.cwd())),
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
export async function runGateStatus(options) {
|
|
@@ -71,5 +74,6 @@ export async function runGateStatus(options) {
|
|
|
71
74
|
`Threshold: ${status.thresholdTokens} tokens (from ${status.thresholdSource})`,
|
|
72
75
|
`Outline: ${status.outline}`,
|
|
73
76
|
`Local store version: ${status.storeVersion ?? "no store"}`,
|
|
77
|
+
`Large-read redirect: ${status.redirect.isOn ? "on" : "off"} (from ${status.redirect.source})`,
|
|
74
78
|
].join("\n"));
|
|
75
79
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,11 @@ if (command === "gate" && subcommand !== undefined && HOOK_SUBCOMMANDS.includes(
|
|
|
5
5
|
const { runGateHook } = await import("./lib/gate/hook.js");
|
|
6
6
|
await runGateHook(subcommand);
|
|
7
7
|
}
|
|
8
|
+
else if (command === "context" && process.argv.includes("--hook")) {
|
|
9
|
+
const { runContextHook } = await import("./lib/context/hook.js");
|
|
10
|
+
if ((await runContextHook()) === "deadline")
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
8
13
|
else if (command === "check" && process.argv.includes("--hook")) {
|
|
9
14
|
const { runHookMode } = await import("./lib/harness/hookEntry.js");
|
|
10
15
|
await runHookMode(process.argv[process.argv.indexOf("--hook") + 1] ?? "", process.argv.includes("--protection"));
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { GRAPH_UNAVAILABLE_REASON } from "./graph.js";
|
|
5
|
+
import { renderPack, emptySections } from "./render.js";
|
|
6
|
+
import { extractTerms, isRegisterTerm, resolveObjects, resolvePaths } from "./resolve.js";
|
|
7
|
+
import { findWriteSites, scanWriteSites, tableSymbols } from "./writers.js";
|
|
8
|
+
export const CONTEXT_EXIT_OK = 0;
|
|
9
|
+
export const CONTEXT_EXIT_NOTHING_RESOLVED = 3;
|
|
10
|
+
export const CONTEXT_EXIT_NO_REGISTER = 4;
|
|
11
|
+
export const CONTEXT_MAX_HOLES = 8;
|
|
12
|
+
export const CARD_MAX_WRITERS = 8;
|
|
13
|
+
const IMPACT_ROWS = 5;
|
|
14
|
+
const TESTS_ROWS = 3;
|
|
15
|
+
const WRITER_ANCHOR_PATHS = 2;
|
|
16
|
+
const HEADER_LINES = 5;
|
|
17
|
+
export const OBLIGATIONS_LINE = "none raised — the rules judge is not installed (program ruling R18)";
|
|
18
|
+
export const JUDGE_LINE = "judge: not installed — exact matches only";
|
|
19
|
+
export const REFUSALS_LINE = "refusals: not extracted — no refusal extractor is installed";
|
|
20
|
+
function line(label, text, extra = {}) {
|
|
21
|
+
return { label, text, ...extra };
|
|
22
|
+
}
|
|
23
|
+
function grepDirOf(root) {
|
|
24
|
+
return existsSync(path.join(root, "packages")) ? "packages" : ".";
|
|
25
|
+
}
|
|
26
|
+
function labelOf(confidence) {
|
|
27
|
+
if (confidence === "certain")
|
|
28
|
+
return "CERTAIN";
|
|
29
|
+
if (confidence === "unresolved")
|
|
30
|
+
return "UNRESOLVED";
|
|
31
|
+
return "INFERRED";
|
|
32
|
+
}
|
|
33
|
+
function cardHeader(object) {
|
|
34
|
+
const scope = object.scope.length === 0 ? "unscoped" : object.scope.join(", ");
|
|
35
|
+
return `object ${object.name} (${object.plane ?? "no plane"} · scope ${scope})`;
|
|
36
|
+
}
|
|
37
|
+
export function cardLines(object, writers, graphReason) {
|
|
38
|
+
const group = object.name;
|
|
39
|
+
const lines = [line("AUTHORED", cardHeader(object), { group })];
|
|
40
|
+
if (object.aliases.length > 0)
|
|
41
|
+
lines.push(line("AUTHORED", `aliases: ${object.aliases.join(", ")}`, { group }));
|
|
42
|
+
if (object.tables.length > 0)
|
|
43
|
+
lines.push(line("AUTHORED", `tables: ${object.tables.join(", ")}`, { group }));
|
|
44
|
+
if (object.oneLine !== null)
|
|
45
|
+
lines.push(line("AUTHORED", object.oneLine, { group }));
|
|
46
|
+
if (writers === null) {
|
|
47
|
+
lines.push(line("NOT KNOWN", graphReason ?? GRAPH_UNAVAILABLE_REASON, { group, role: "writer" }));
|
|
48
|
+
}
|
|
49
|
+
else if (object.tables.length > 0) {
|
|
50
|
+
if (writers.length === 0)
|
|
51
|
+
lines.push(line("INFERRED", `writers: no name-matched write site of ${object.tables.join(", ")}`, { group }));
|
|
52
|
+
for (const site of writers.slice(0, CARD_MAX_WRITERS)) {
|
|
53
|
+
lines.push(line("INFERRED", `writer ${site.path}:${site.line} ${site.verb} ${site.table}`, { group, role: "writer", paths: [site.path] }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.push(line("UNRESOLVED", REFUSALS_LINE, { group }));
|
|
57
|
+
for (const claim of object.claims) {
|
|
58
|
+
const text = claim.type === "predicate" ? `claim ${claim.text} (not evaluated)` : `claim? ${claim.text}`;
|
|
59
|
+
lines.push(line("AUTHORED", text, { group, role: "claim" }));
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
export function cardHoles(object, writers, dir) {
|
|
64
|
+
const group = object.name;
|
|
65
|
+
const holes = object.tables.map((table) => line("NOT KNOWN", `grep -rnE "(INTO|UPDATE|FROM) ${table}\\b" ${dir}`, { group }));
|
|
66
|
+
const writerFile = writers?.[0]?.path;
|
|
67
|
+
holes.push(line("NOT KNOWN", `grep -rnE "fail\\(c, [0-9]{3}" ${writerFile ?? dir}`, { group }));
|
|
68
|
+
return holes;
|
|
69
|
+
}
|
|
70
|
+
export function objectWriters(root, object, graph, scrutinised = []) {
|
|
71
|
+
const sites = [];
|
|
72
|
+
for (const table of object.tables) {
|
|
73
|
+
for (const site of findWriteSites(root, table, graph, scrutinised)) {
|
|
74
|
+
if (!sites.some((existing) => existing.path === site.path && existing.line === site.line))
|
|
75
|
+
sites.push(site);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return sites;
|
|
79
|
+
}
|
|
80
|
+
function answerRows(answer, fn, target, limit, skip) {
|
|
81
|
+
if (answer === null)
|
|
82
|
+
return [];
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const section of answer.sections) {
|
|
85
|
+
const label = labelOf(section.confidence);
|
|
86
|
+
if (section.rows.length === 0) {
|
|
87
|
+
lines.push(line(label, `${fn} ${target} — ${section.label}: none (${section.emptyMeans ?? "no rows"})`));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const row of section.rows) {
|
|
91
|
+
if (row.at !== undefined && skip.has(row.at))
|
|
92
|
+
continue;
|
|
93
|
+
const file = (row.at ?? row.text).replace(/:\d+$/, "");
|
|
94
|
+
const text = `${fn} ${target} — ${section.label}: ${row.text}${row.at !== undefined && row.at !== row.text ? ` ${row.at}` : ""}${row.note !== undefined ? ` (${row.note})` : ""}`;
|
|
95
|
+
lines.push(line(label, text, { paths: [file] }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.slice(0, limit);
|
|
99
|
+
}
|
|
100
|
+
function frontmatterStatus(root, doc) {
|
|
101
|
+
try {
|
|
102
|
+
const head = readFileSync(path.join(root, doc), "utf8").slice(0, 4_000);
|
|
103
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(head);
|
|
104
|
+
const status = match === null ? null : /^status:\s*(.+)$/m.exec(match[1] ?? "");
|
|
105
|
+
return status === null ? "no status" : (status[1] ?? "").trim();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return "unreadable";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function documentsByObjects(root, docsRoot, names) {
|
|
112
|
+
if (names.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
let listing;
|
|
115
|
+
try {
|
|
116
|
+
listing = execFileSync("git", ["grep", "-l", "-E", "^objects:", "--", `${docsRoot}/**/*.md`, `${docsRoot}/*.md`], {
|
|
117
|
+
cwd: root,
|
|
118
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const found = [];
|
|
126
|
+
for (const doc of listing.split("\n").filter((entry) => entry !== "")) {
|
|
127
|
+
let objectsLine = "";
|
|
128
|
+
try {
|
|
129
|
+
objectsLine = /^objects:(.*)$/m.exec(readFileSync(path.join(root, doc), "utf8").slice(0, 4_000))?.[1] ?? "";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
if (new RegExp(`(^|[^A-Za-z0-9_-])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}($|[^A-Za-z0-9_-])`).test(objectsLine)) {
|
|
136
|
+
found.push({ doc, object: name });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
function headerPointers(root, docsRoot, file) {
|
|
143
|
+
try {
|
|
144
|
+
const head = readFileSync(path.join(root, file), "utf8").split("\n").slice(0, HEADER_LINES).join("\n");
|
|
145
|
+
const pattern = new RegExp(`${docsRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/[A-Za-z0-9_./-]+\\.md`, "g");
|
|
146
|
+
return [...new Set(head.match(pattern) ?? [])].filter((doc) => existsSync(path.join(root, doc)));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function composePack(input) {
|
|
153
|
+
const { root, register, graph } = input;
|
|
154
|
+
const sections = emptySections();
|
|
155
|
+
const empty = {};
|
|
156
|
+
const dir = grepDirOf(root);
|
|
157
|
+
const terms = extractTerms(input.text);
|
|
158
|
+
const { resolved, missing } = resolvePaths(root, input.cwd, [...input.paths, ...terms.paths]);
|
|
159
|
+
const files = resolved.filter((entry) => entry.isFile).map((entry) => entry.path);
|
|
160
|
+
const pathSites = [];
|
|
161
|
+
for (const object of register.objects) {
|
|
162
|
+
for (const table of object.tables)
|
|
163
|
+
pathSites.push(...scanWriteSites(root, files, table, tableSymbols(table, graph)));
|
|
164
|
+
}
|
|
165
|
+
const resolutions = resolveObjects(register.objects, input.text, terms.identifiers, pathSites);
|
|
166
|
+
for (const entry of resolved)
|
|
167
|
+
sections.RESOLVED.push(line("CERTAIN", `path ${entry.path}`, { paths: entry.isFile ? [entry.path] : [] }));
|
|
168
|
+
for (const resolution of resolutions)
|
|
169
|
+
sections.RESOLVED.push(line("AUTHORED", `${resolution.object.name} via ${resolution.via.join(", ")}`));
|
|
170
|
+
sections.RESOLVED.push(line("CERTAIN", JUDGE_LINE));
|
|
171
|
+
if (graph !== null && graph.indexedHead !== null && input.checkoutHead !== null && graph.indexedHead !== input.checkoutHead) {
|
|
172
|
+
sections.RESOLVED.push(line("CERTAIN", `index at ${graph.indexedHead.slice(0, 7)}, checkout at ${input.checkoutHead.slice(0, 7)}`));
|
|
173
|
+
}
|
|
174
|
+
const cardHoleLines = [];
|
|
175
|
+
const writerSites = new Set();
|
|
176
|
+
const writerFiles = [];
|
|
177
|
+
for (const { object } of resolutions) {
|
|
178
|
+
const writers = graph === null ? null : objectWriters(root, object, graph, files);
|
|
179
|
+
for (const site of writers ?? []) {
|
|
180
|
+
writerSites.add(`${site.path}:${site.line}`);
|
|
181
|
+
if (!writerFiles.includes(site.path))
|
|
182
|
+
writerFiles.push(site.path);
|
|
183
|
+
}
|
|
184
|
+
sections.CARDS.push(...cardLines(object, writers, input.graphReason));
|
|
185
|
+
cardHoleLines.push(...cardHoles(object, writers, dir));
|
|
186
|
+
}
|
|
187
|
+
const resolvedNames = new Set(resolutions.map((resolution) => resolution.object.name));
|
|
188
|
+
for (const { object } of resolutions) {
|
|
189
|
+
if (object.projects !== null)
|
|
190
|
+
sections.RELATIONS.push(line("AUTHORED", `${object.name} projects ${object.projects}`));
|
|
191
|
+
}
|
|
192
|
+
const coWritten = new Set();
|
|
193
|
+
for (const file of writerFiles) {
|
|
194
|
+
for (const other of register.objects) {
|
|
195
|
+
if (other.tables.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
const isWritten = other.tables.some((table) => scanWriteSites(root, [file], table, tableSymbols(table, graph)).length > 0);
|
|
198
|
+
const key = `${other.name}@${file}`;
|
|
199
|
+
if (!isWritten || coWritten.has(key))
|
|
200
|
+
continue;
|
|
201
|
+
if (resolvedNames.has(other.name))
|
|
202
|
+
continue;
|
|
203
|
+
coWritten.add(key);
|
|
204
|
+
sections.RELATIONS.push(line("INFERRED", `${other.name} co-written in ${file}`, { paths: [file] }));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (graph === null) {
|
|
208
|
+
empty.ANCHORS = input.graphReason ?? GRAPH_UNAVAILABLE_REASON;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const anchorPaths = files.length > 0 ? files : writerFiles.slice(0, WRITER_ANCHOR_PATHS);
|
|
212
|
+
for (const target of anchorPaths) {
|
|
213
|
+
const rows = [
|
|
214
|
+
...answerRows(graph.ask("impact", { target, limit: IMPACT_ROWS }), "impact", target, IMPACT_ROWS, writerSites),
|
|
215
|
+
...answerRows(graph.ask("tests-covering", { path: target, limit: TESTS_ROWS }), "tests-covering", target, TESTS_ROWS, writerSites),
|
|
216
|
+
];
|
|
217
|
+
rows.forEach((row, rank) => sections.ANCHORS.push({ ...row, group: target, rank }));
|
|
218
|
+
}
|
|
219
|
+
if (anchorPaths.length === 0)
|
|
220
|
+
empty.ANCHORS = "no path resolved and no writer found";
|
|
221
|
+
}
|
|
222
|
+
const seenDocs = new Set();
|
|
223
|
+
for (const { doc, object } of documentsByObjects(root, input.docsRoot, [...resolvedNames])) {
|
|
224
|
+
if (seenDocs.has(doc))
|
|
225
|
+
continue;
|
|
226
|
+
seenDocs.add(doc);
|
|
227
|
+
sections.DOCS.push(line("AUTHORED", `${doc} (status ${frontmatterStatus(root, doc)}) — designs ${object}`, { paths: [doc] }));
|
|
228
|
+
}
|
|
229
|
+
for (const source of [...files, ...writerFiles]) {
|
|
230
|
+
for (const doc of headerPointers(root, input.docsRoot, source)) {
|
|
231
|
+
if (seenDocs.has(doc))
|
|
232
|
+
continue;
|
|
233
|
+
seenDocs.add(doc);
|
|
234
|
+
sections.DOCS.push(line("AUTHORED", `${doc} (status ${frontmatterStatus(root, doc)}) — named by the header of ${source}`, { paths: [doc] }));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
sections.OBLIGATIONS.push(line("CERTAIN", OBLIGATIONS_LINE));
|
|
238
|
+
const holes = [];
|
|
239
|
+
for (const term of terms.identifiers) {
|
|
240
|
+
if (isRegisterTerm(register.objects, term))
|
|
241
|
+
continue;
|
|
242
|
+
if (graph !== null && graph.isDefined(term))
|
|
243
|
+
continue;
|
|
244
|
+
holes.push(line("NOT KNOWN", `grep -rn "${term}" ${dir}`));
|
|
245
|
+
}
|
|
246
|
+
for (const term of missing)
|
|
247
|
+
holes.push(line("NOT KNOWN", `grep -rn "${path.basename(term)}" ${dir}`));
|
|
248
|
+
for (const refusal of register.refusals) {
|
|
249
|
+
holes.push(line("NOT KNOWN", `grep -n "${refusal.name ?? ""}" ${refusal.file} (refused at line ${refusal.line}: ${refusal.reason})`));
|
|
250
|
+
}
|
|
251
|
+
holes.push(...cardHoleLines);
|
|
252
|
+
sections.HOLES = holes.slice(0, CONTEXT_MAX_HOLES);
|
|
253
|
+
const rendered = renderPack(sections, empty, input.budgetTokens);
|
|
254
|
+
const offered = new Set();
|
|
255
|
+
for (const name of ["RESOLVED", "CARDS", "RELATIONS", "ANCHORS", "DOCS"]) {
|
|
256
|
+
for (const kept of rendered.sections[name]) {
|
|
257
|
+
for (const file of kept.paths ?? [])
|
|
258
|
+
if (existsSync(path.join(root, file)))
|
|
259
|
+
offered.add(file);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
exitCode: resolutions.length === 0 && resolved.length === 0 ? CONTEXT_EXIT_NOTHING_RESOLVED : CONTEXT_EXIT_OK,
|
|
264
|
+
objects: resolutions.map((resolution) => resolution.object.name),
|
|
265
|
+
resolvedPaths: resolved.map((entry) => entry.path),
|
|
266
|
+
rendered,
|
|
267
|
+
offeredPaths: [...offered],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CARD_MAX_WRITERS } from "./compose.js";
|
|
2
|
+
import { renderCard } from "./render.js";
|
|
3
|
+
import { camelCaseOf, findWriteSites, scanWriteSites } from "./writers.js";
|
|
4
|
+
export function objectsWrittenIn(root, register, relPath) {
|
|
5
|
+
return register.objects.filter((object) => object.tables.some((table) => scanWriteSites(root, [relPath], table, [camelCaseOf(table)]).length > 0));
|
|
6
|
+
}
|
|
7
|
+
export function buildEditCard(root, object, relPath, graph, graphReason) {
|
|
8
|
+
const group = object.name;
|
|
9
|
+
const scope = object.scope.length === 0 ? "unscoped" : object.scope.join(", ");
|
|
10
|
+
const lines = [{ label: "AUTHORED", text: `object ${object.name} (${object.plane ?? "no plane"} · scope ${scope})`, group }];
|
|
11
|
+
if (object.oneLine !== null)
|
|
12
|
+
lines.push({ label: "AUTHORED", text: object.oneLine, group });
|
|
13
|
+
const writers = [];
|
|
14
|
+
for (const table of object.tables) {
|
|
15
|
+
const sites = graph === null ? scanWriteSites(root, [relPath], table, [camelCaseOf(table)]) : findWriteSites(root, table, graph, [relPath]);
|
|
16
|
+
for (const site of sites)
|
|
17
|
+
if (!writers.some((existing) => existing.path === site.path && existing.line === site.line))
|
|
18
|
+
writers.push(site);
|
|
19
|
+
}
|
|
20
|
+
if (graph === null)
|
|
21
|
+
lines.push({ label: "NOT KNOWN", text: graphReason ?? "code graph not available — `nustack graph index`", group });
|
|
22
|
+
for (const site of writers.slice(0, CARD_MAX_WRITERS)) {
|
|
23
|
+
lines.push({ label: "INFERRED", text: `writer ${site.path}:${site.line} ${site.verb} ${site.table}`, group, role: "writer" });
|
|
24
|
+
}
|
|
25
|
+
lines.push({ label: "NOT KNOWN", text: `grep -rnE "fail\\(c, [0-9]{3}" ${relPath}`, group });
|
|
26
|
+
for (const claim of object.claims) {
|
|
27
|
+
const text = claim.type === "predicate" ? `claim ${claim.text} (not evaluated)` : `claim? ${claim.text}`;
|
|
28
|
+
lines.push({ label: "AUTHORED", text, group, role: "claim" });
|
|
29
|
+
}
|
|
30
|
+
return renderCard(`EDIT CARD — ${relPath} writes ${object.name}`, lines);
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadCodegraph } from "../graph/library.js";
|
|
4
|
+
import { refreshIfStale } from "../graph/freshness.js";
|
|
5
|
+
export const GRAPH_UNAVAILABLE_REASON = "code graph not available — `nustack graph index`";
|
|
6
|
+
function viewOver(library, root, db, indexedHead) {
|
|
7
|
+
const ask = (fn, args) => {
|
|
8
|
+
try {
|
|
9
|
+
const answer = library.dispatch({ root, db, budget: 0 }, { fn, args });
|
|
10
|
+
return answer.unavailable === undefined ? answer : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const defined = new Map();
|
|
17
|
+
return {
|
|
18
|
+
indexedHead,
|
|
19
|
+
ask,
|
|
20
|
+
isDefined(symbol) {
|
|
21
|
+
const known = defined.get(symbol);
|
|
22
|
+
if (known !== undefined)
|
|
23
|
+
return known;
|
|
24
|
+
const answer = ask("where-defined", { symbol, limit: 1 });
|
|
25
|
+
const isDefined = (answer?.sections[0]?.rows.length ?? 0) > 0;
|
|
26
|
+
defined.set(symbol, isDefined);
|
|
27
|
+
return isDefined;
|
|
28
|
+
},
|
|
29
|
+
importers(symbol, limit) {
|
|
30
|
+
const answer = ask("refs", { name: symbol, limit });
|
|
31
|
+
const section = answer?.sections.find((candidate) => candidate.label === "imported at");
|
|
32
|
+
const files = (section?.rows ?? []).map((row) => (row.at ?? "").replace(/:\d+$/, "")).filter((file) => file !== "");
|
|
33
|
+
return [...new Set(files)].slice(0, limit);
|
|
34
|
+
},
|
|
35
|
+
close() {
|
|
36
|
+
try {
|
|
37
|
+
db.close();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function openGraphForHook(root, load = loadCodegraph) {
|
|
45
|
+
let library;
|
|
46
|
+
try {
|
|
47
|
+
library = await load();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { view: null, reason: GRAPH_UNAVAILABLE_REASON };
|
|
51
|
+
}
|
|
52
|
+
if (typeof library.openReadOnly !== "function")
|
|
53
|
+
return { view: null, reason: GRAPH_UNAVAILABLE_REASON };
|
|
54
|
+
const db = library.openReadOnly(root);
|
|
55
|
+
if (db === null)
|
|
56
|
+
return { view: null, reason: GRAPH_UNAVAILABLE_REASON };
|
|
57
|
+
const record = library.findProject(root);
|
|
58
|
+
return { view: viewOver(library, root, db, record?.lastHead ?? null), reason: null };
|
|
59
|
+
}
|
|
60
|
+
export async function openGraphForCommand(root, options) {
|
|
61
|
+
let library;
|
|
62
|
+
try {
|
|
63
|
+
library = await loadCodegraph();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { view: null, reason: GRAPH_UNAVAILABLE_REASON };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
await refreshIfStale(library, root, { noRefresh: options.noRefresh, onProgress: options.onProgress });
|
|
70
|
+
const record = library.findProject(root);
|
|
71
|
+
const db = library.open(root);
|
|
72
|
+
return { view: viewOver(library, root, db, record?.lastHead ?? null), reason: null };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { view: null, reason: GRAPH_UNAVAILABLE_REASON };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function readCheckoutHead(root) {
|
|
79
|
+
try {
|
|
80
|
+
let gitDir = path.join(root, ".git");
|
|
81
|
+
if (!existsSync(path.join(gitDir, "HEAD"))) {
|
|
82
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(gitDir, "utf8"));
|
|
83
|
+
if (pointer === null)
|
|
84
|
+
return null;
|
|
85
|
+
gitDir = path.resolve(root, (pointer[1] ?? "").trim());
|
|
86
|
+
}
|
|
87
|
+
const head = readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
|
|
88
|
+
const ref = /^ref:\s*(.+)$/.exec(head);
|
|
89
|
+
if (ref === null)
|
|
90
|
+
return /^[0-9a-f]{40}$/.test(head) ? head : null;
|
|
91
|
+
const name = (ref[1] ?? "").trim();
|
|
92
|
+
const commonDir = existsSync(path.join(gitDir, "commondir"))
|
|
93
|
+
? path.resolve(gitDir, readFileSync(path.join(gitDir, "commondir"), "utf8").trim())
|
|
94
|
+
: gitDir;
|
|
95
|
+
for (const dir of [gitDir, commonDir]) {
|
|
96
|
+
const loose = path.join(dir, name);
|
|
97
|
+
if (existsSync(loose))
|
|
98
|
+
return readFileSync(loose, "utf8").trim();
|
|
99
|
+
}
|
|
100
|
+
const packed = readFileSync(path.join(commonDir, "packed-refs"), "utf8");
|
|
101
|
+
const line = packed.split("\n").find((entry) => entry.endsWith(` ${name}`));
|
|
102
|
+
return line === undefined ? null : (line.split(" ")[0] ?? null);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|