@nimiplatform/nimi-coding 0.5.0 → 0.6.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/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +50 -25
- package/README.zh-CN.md +50 -25
- package/cli/commands/clear.mjs +3 -2
- package/cli/commands/code.mjs +225 -0
- package/cli/commands/start.mjs +3 -2
- package/cli/constants.mjs +2 -1
- package/cli/dev/run-cli-smoke.mjs +6 -0
- package/cli/help.mjs +8 -3
- package/cli/index.mjs +3 -2
- package/cli/lib/authority/diagnostics.mjs +3 -1
- package/cli/lib/code/authority-annotation-response.mjs +43 -0
- package/cli/lib/code/authority-annotation-scan.mjs +162 -0
- package/cli/lib/code/authority-annotations.mjs +293 -0
- package/cli/lib/code/context.mjs +710 -0
- package/cli/lib/code/response.mjs +165 -0
- package/cli/lib/doctor.mjs +3 -2
- package/cli/lib/entrypoints.mjs +12 -5
- package/cli/seeds/seed-policy.yaml +2 -2
- package/methodology/authority-authoring.yaml +67 -52
- package/methodology/core.yaml +6 -4
- package/package.json +5 -3
- package/spec/code-authority-annotations.yaml +42 -0
- package/spec/code-context.yaml +38 -0
- package/spec/product-scope.yaml +8 -3
- package/cli/commands/validate-ai-governance.mjs +0 -335
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
- package/cli/lib/internal/governance/config.mjs +0 -114
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
package/cli/constants.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.6.0";
|
|
2
2
|
export const PACKAGE_NAME = "@nimiplatform/nimi-coding";
|
|
3
|
+
export const PROJECT_LOCAL_CLI_INVOCATION = "pnpm exec nimicoding";
|
|
3
4
|
export const LOCAL_GITIGNORE_ENTRIES = [".nimi/local/"];
|
|
4
5
|
export const AUTHORITY_GITATTRIBUTES_ENTRIES = [
|
|
5
6
|
"*.authority.yaml text eol=lf",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
6
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
6
7
|
const cliPath = path.join(repoRoot, "bin", "nimicoding.mjs");
|
|
8
|
+
const packageVersion = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
|
|
7
9
|
|
|
8
10
|
for (const args of [["--help"], ["--version"]]) {
|
|
9
11
|
const result = spawnSync(process.execPath, [cliPath, ...args], {
|
|
@@ -21,4 +23,8 @@ for (const args of [["--help"], ["--version"]]) {
|
|
|
21
23
|
process.stderr.write(`${args.join(" ")} smoke exited with ${result.status ?? "unknown"}\n`);
|
|
22
24
|
process.exit(result.status ?? 1);
|
|
23
25
|
}
|
|
26
|
+
if (args[0] === "--version" && result.stdout !== `${packageVersion}\n`) {
|
|
27
|
+
process.stderr.write(`--version returned ${JSON.stringify(result.stdout)}; expected ${JSON.stringify(`${packageVersion}\n`)}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
24
30
|
}
|
package/cli/help.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VERSION } from "./constants.mjs";
|
|
2
2
|
import { AUTHORITY_ANCHOR_GRAMMAR_HELP } from "./lib/authority/anchors.mjs";
|
|
3
3
|
import { AUTHORITY_COMMAND_USAGE } from "./commands/authority.mjs";
|
|
4
|
+
import { CODE_COMMAND_USAGE } from "./commands/code.mjs";
|
|
4
5
|
import { localize, styleCommand, styleHeading, styleMuted } from "./lib/ui.mjs";
|
|
5
6
|
|
|
6
7
|
export function helpText() {
|
|
@@ -8,11 +9,11 @@ export function helpText() {
|
|
|
8
9
|
// The authority list comes from the command module itself so help and the
|
|
9
10
|
// parser cannot describe different surfaces.
|
|
10
11
|
...AUTHORITY_COMMAND_USAGE,
|
|
12
|
+
...CODE_COMMAND_USAGE,
|
|
11
13
|
"nimicoding start [--yes]",
|
|
12
14
|
"nimicoding sync [--apply|--check|--dry-run] [--json]",
|
|
13
15
|
"nimicoding clear [--yes]",
|
|
14
16
|
"nimicoding doctor [--verbose|--json]",
|
|
15
|
-
"nimicoding validate-ai-governance --profile <profile-id> --scope <all|agents-freshness|context-budget|structure-budget|high-risk-doc-metadata> # optional L3 repository governance",
|
|
16
17
|
];
|
|
17
18
|
return [
|
|
18
19
|
styleHeading(`nimicoding ${VERSION}`),
|
|
@@ -25,8 +26,12 @@ export function helpText() {
|
|
|
25
26
|
AUTHORITY_ANCHOR_GRAMMAR_HELP,
|
|
26
27
|
"",
|
|
27
28
|
styleMuted(localize(
|
|
28
|
-
"Nimi Coding provides canonical authority
|
|
29
|
-
"Nimi Coding 提供 canonical authority
|
|
29
|
+
"Nimi Coding provides canonical authority products, bounded read-only TypeScript context, and explicit code-authority marker lookup. AI task planning, editing, verification, and execution belong to the host.",
|
|
30
|
+
"Nimi Coding 提供 canonical authority 产品、有界只读的 TypeScript context,以及显式 code-authority marker 查询。AI 任务规划、编辑、验证与执行由宿主负责。",
|
|
31
|
+
)),
|
|
32
|
+
styleMuted(localize(
|
|
33
|
+
"Code queries read tracked current-filesystem source; unsaved editor buffers, conformance claims, and unannotated implementation semantics are outside v1.",
|
|
34
|
+
"Code 查询读取 Git tracked 的当前文件系统源码;未保存的编辑器 buffer、conformance 声明和未标注实现语义不属于 v1。",
|
|
30
35
|
)),
|
|
31
36
|
"",
|
|
32
37
|
].join("\n");
|
package/cli/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { runAuthority } from "./commands/authority.mjs";
|
|
2
2
|
import { runClear } from "./commands/clear.mjs";
|
|
3
|
+
import { classifyCodeInvocation, runCodeInvocation } from "./commands/code.mjs";
|
|
3
4
|
import { runDoctor } from "./commands/doctor.mjs";
|
|
4
5
|
import { runStart } from "./commands/start.mjs";
|
|
5
6
|
import { runSync } from "./commands/sync.mjs";
|
|
6
|
-
import { runValidateAiGovernance } from "./commands/validate-ai-governance.mjs";
|
|
7
7
|
import { helpText } from "./help.mjs";
|
|
8
8
|
import { VERSION } from "./constants.mjs";
|
|
9
9
|
import { configureCliUi, localize, parseGlobalUiOptions } from "./lib/ui.mjs";
|
|
@@ -14,10 +14,11 @@ const COMMANDS = {
|
|
|
14
14
|
sync: runSync,
|
|
15
15
|
clear: runClear,
|
|
16
16
|
doctor: runDoctor,
|
|
17
|
-
"validate-ai-governance": runValidateAiGovernance,
|
|
18
17
|
};
|
|
19
18
|
|
|
20
19
|
export async function runCli(args) {
|
|
20
|
+
const codeCommandIndex = classifyCodeInvocation(args);
|
|
21
|
+
if (codeCommandIndex !== null) return runCodeInvocation(args, codeCommandIndex);
|
|
21
22
|
const parsedUi = parseGlobalUiOptions(args);
|
|
22
23
|
if (!parsedUi.ok) {
|
|
23
24
|
process.stderr.write(parsedUi.error);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { PROJECT_LOCAL_CLI_INVOCATION } from "../../constants.mjs";
|
|
4
|
+
|
|
3
5
|
export function compareText(left, right) {
|
|
4
6
|
const a = String(left);
|
|
5
7
|
const b = String(right);
|
|
@@ -89,5 +91,5 @@ export const REPAIRS = {
|
|
|
89
91
|
invalid: "replace the value with one admitted by the authoring contract",
|
|
90
92
|
relation: "repair the explicit relation using product authority; do not infer a target",
|
|
91
93
|
duplicate: "keep one declaration chosen from product authority and remove the duplicate",
|
|
92
|
-
format:
|
|
94
|
+
format: `run \`${PROJECT_LOCAL_CLI_INVOCATION} authority fmt\` on this file`,
|
|
93
95
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
export const CODE_AUTHORITY_FORMAT = "nimicoding.code-authority-response/v1";
|
|
4
|
+
export const MIN_CODE_AUTHORITY_BYTES = 512;
|
|
5
|
+
|
|
6
|
+
const MAX_ERROR_MESSAGE_BYTES = 160;
|
|
7
|
+
|
|
8
|
+
export function refusedCodeAuthorityResponse(code, message) {
|
|
9
|
+
if (Buffer.byteLength(message, "utf8") > MAX_ERROR_MESSAGE_BYTES) {
|
|
10
|
+
throw new Error("code authority error message exceeds its fixed protocol bound");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
format: CODE_AUTHORITY_FORMAT,
|
|
14
|
+
status: "refused",
|
|
15
|
+
complete: false,
|
|
16
|
+
operation: null,
|
|
17
|
+
scope: null,
|
|
18
|
+
result: null,
|
|
19
|
+
findings: null,
|
|
20
|
+
counts: null,
|
|
21
|
+
usage: null,
|
|
22
|
+
error: { code, message },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completedCodeAuthorityResponse(value) {
|
|
27
|
+
return {
|
|
28
|
+
format: CODE_AUTHORITY_FORMAT,
|
|
29
|
+
status: value.operation === "lifecycle-audit" && value.findings.length > 0 ? "findings" : "completed",
|
|
30
|
+
complete: true,
|
|
31
|
+
...value,
|
|
32
|
+
error: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function serializeCodeAuthorityResponse(response) {
|
|
37
|
+
return `${JSON.stringify(response)}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderCodeAuthorityResponse(response, maxBytes) {
|
|
41
|
+
const text = serializeCodeAuthorityResponse(response);
|
|
42
|
+
return Buffer.byteLength(text, "utf8") <= maxBytes ? text : null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
|
|
5
|
+
import { compareText, createLocator } from "../authority/diagnostics.mjs";
|
|
6
|
+
|
|
7
|
+
const EXACT_MARKER = /^\/\/ nimi-(authority|deprecated): ([^\s]+)$/u;
|
|
8
|
+
const MARKER_CANDIDATE = /^\/\/[ \t]*nimi-(?:authority|deprecated)\b/u;
|
|
9
|
+
const PHYSICAL_CANDIDATE = /^[ \t]*\/\/[ \t]*nimi-(?:authority|deprecated)\b/mu;
|
|
10
|
+
const NEARBY_SOURCE_BYTES = 512;
|
|
11
|
+
|
|
12
|
+
function comparePosition(left, right) {
|
|
13
|
+
return left.line - right.line || left.column - right.column;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function utf8Prefix(text, maxBytes) {
|
|
17
|
+
let value = "";
|
|
18
|
+
let bytes = 0;
|
|
19
|
+
for (const character of text) {
|
|
20
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
21
|
+
if (bytes + size > maxBytes) break;
|
|
22
|
+
value += character;
|
|
23
|
+
bytes += size;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nearbySource(path, text, locator, markerEnd) {
|
|
29
|
+
const start = markerEnd < text.length && text[markerEnd] === "\r" ? markerEnd + 1 : markerEnd;
|
|
30
|
+
const contentStart = start < text.length && text[start] === "\n" ? start + 1 : start;
|
|
31
|
+
const candidate = text.slice(contentStart);
|
|
32
|
+
const value = utf8Prefix(candidate, NEARBY_SOURCE_BYTES);
|
|
33
|
+
return {
|
|
34
|
+
location: { path, range: locator.range(contentStart, contentStart + value.length) },
|
|
35
|
+
text: value,
|
|
36
|
+
truncated: value.length < candidate.length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseComment(path, text, locator, start, end, identifier) {
|
|
41
|
+
const lineStart = text.lastIndexOf("\n", start - 1) + 1;
|
|
42
|
+
if (text.slice(lineStart, start).trim().length > 0) return { occurrence: null, finding: null };
|
|
43
|
+
const raw = text.slice(start, end).replace(/\r$/u, "");
|
|
44
|
+
if (!MARKER_CANDIDATE.test(raw)) return { occurrence: null, finding: null };
|
|
45
|
+
const location = { path, range: locator.range(start, end) };
|
|
46
|
+
const match = EXACT_MARKER.exec(raw);
|
|
47
|
+
if (match === null || !identifier.test(match[2])) {
|
|
48
|
+
return {
|
|
49
|
+
occurrence: null,
|
|
50
|
+
finding: {
|
|
51
|
+
kind: "invalid-annotation",
|
|
52
|
+
source_path: path,
|
|
53
|
+
annotation_location: location,
|
|
54
|
+
reason: "annotation must match the exact standalone exact-ID-only line grammar",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
occurrence: {
|
|
60
|
+
annotation_kind: match[1],
|
|
61
|
+
authority_id: match[2],
|
|
62
|
+
source_path: path,
|
|
63
|
+
annotation_location: location,
|
|
64
|
+
nearby_source: nearbySource(path, text, locator, end),
|
|
65
|
+
},
|
|
66
|
+
finding: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function typeScriptLineComments(path, text) {
|
|
71
|
+
const scriptKind = path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
72
|
+
const sourceFile = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind);
|
|
73
|
+
const comments = new Map();
|
|
74
|
+
const jsxTextRanges = [];
|
|
75
|
+
function add(ranges) {
|
|
76
|
+
for (const range of ranges ?? []) {
|
|
77
|
+
if (range.kind === ts.SyntaxKind.SingleLineCommentTrivia) {
|
|
78
|
+
comments.set(`${range.pos}:${range.end}`, { start: range.pos, end: range.end });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const pending = [sourceFile];
|
|
83
|
+
while (pending.length > 0) {
|
|
84
|
+
const node = pending.pop();
|
|
85
|
+
if (ts.isJsxText(node)) jsxTextRanges.push({ start: node.pos, end: node.end });
|
|
86
|
+
add(ts.getLeadingCommentRanges(text, node.pos));
|
|
87
|
+
add(ts.getTrailingCommentRanges(text, node.end));
|
|
88
|
+
pending.push(...node.getChildren(sourceFile));
|
|
89
|
+
}
|
|
90
|
+
return [...comments.values()]
|
|
91
|
+
.filter((comment) => !jsxTextRanges.some((range) => comment.start >= range.start && comment.end <= range.end))
|
|
92
|
+
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function goLineComments(text) {
|
|
96
|
+
const comments = [];
|
|
97
|
+
let index = 0;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const character = text[index];
|
|
100
|
+
if (character === "/" && text[index + 1] === "/") {
|
|
101
|
+
const start = index;
|
|
102
|
+
index += 2;
|
|
103
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
104
|
+
comments.push({ start, end: index });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (character === "/" && text[index + 1] === "*") {
|
|
108
|
+
const close = text.indexOf("*/", index + 2);
|
|
109
|
+
index = close < 0 ? text.length : close + 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (character === "`" || character === "\"" || character === "'") {
|
|
113
|
+
const quote = character;
|
|
114
|
+
index += 1;
|
|
115
|
+
while (index < text.length) {
|
|
116
|
+
if (quote !== "`" && text[index] === "\\") {
|
|
117
|
+
index += Math.min(2, text.length - index);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (text[index] === quote) {
|
|
121
|
+
index += 1;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
index += 1;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
return comments;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function scanAuthorityAnnotations(path, text, identifierPattern) {
|
|
134
|
+
if (!PHYSICAL_CANDIDATE.test(text)) return { occurrences: [], findings: [] };
|
|
135
|
+
const locator = createLocator(text);
|
|
136
|
+
const identifier = new RegExp(identifierPattern, "u");
|
|
137
|
+
const comments = path.endsWith(".go") ? goLineComments(text) : typeScriptLineComments(path, text);
|
|
138
|
+
const occurrences = [];
|
|
139
|
+
const findings = [];
|
|
140
|
+
for (const comment of comments) {
|
|
141
|
+
const parsed = parseComment(path, text, locator, comment.start, comment.end, identifier);
|
|
142
|
+
if (parsed.occurrence !== null) occurrences.push(parsed.occurrence);
|
|
143
|
+
if (parsed.finding !== null) findings.push(parsed.finding);
|
|
144
|
+
}
|
|
145
|
+
occurrences.sort(compareOccurrences);
|
|
146
|
+
findings.sort(compareAnnotationFindings);
|
|
147
|
+
return { occurrences, findings };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function compareOccurrences(left, right) {
|
|
151
|
+
return compareText(left.source_path, right.source_path)
|
|
152
|
+
|| comparePosition(left.annotation_location.range.start, right.annotation_location.range.start)
|
|
153
|
+
|| compareText(left.annotation_kind, right.annotation_kind)
|
|
154
|
+
|| compareText(left.authority_id, right.authority_id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function compareAnnotationFindings(left, right) {
|
|
158
|
+
return compareText(left.source_path, right.source_path)
|
|
159
|
+
|| comparePosition(left.annotation_location.range.start, right.annotation_location.range.start)
|
|
160
|
+
|| compareText(left.kind, right.kind)
|
|
161
|
+
|| compareText(left.reason, right.reason);
|
|
162
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { compileAuthorityPath } from "../authority/compile.mjs";
|
|
5
|
+
import { compareText } from "../authority/diagnostics.mjs";
|
|
6
|
+
import { captureStableRegularFile } from "../authority/git-snapshot.mjs";
|
|
7
|
+
import { buildAuthorityGraphSnapshot } from "../authority/graph.mjs";
|
|
8
|
+
import { repositoryInventory } from "../authority/repository-inventory.mjs";
|
|
9
|
+
import { resolveRepositoryCorpus } from "../authority/repository-corpus.mjs";
|
|
10
|
+
import { loadAuthorityContract } from "../authority/source-yaml.mjs";
|
|
11
|
+
import {
|
|
12
|
+
compareAnnotationFindings,
|
|
13
|
+
compareOccurrences,
|
|
14
|
+
scanAuthorityAnnotations,
|
|
15
|
+
} from "./authority-annotation-scan.mjs";
|
|
16
|
+
|
|
17
|
+
const SUPPORTED_SOURCE = /(?:\.d)?\.tsx?$|\.go$/u;
|
|
18
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
19
|
+
const CLASSIFICATION_ORDER = new Map([
|
|
20
|
+
["active-reference", 0],
|
|
21
|
+
["removed-reference", 1],
|
|
22
|
+
["superseded-reference", 2],
|
|
23
|
+
["missing-reference", 3],
|
|
24
|
+
["explicit-deprecated", 4],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export class CodeAuthorityFailure extends Error {
|
|
28
|
+
constructor(code, message, exitCode = 2) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "CodeAuthorityFailure";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.exitCode = exitCode;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(code, message, exitCode = 2) {
|
|
37
|
+
throw new CodeAuthorityFailure(code, message, exitCode);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareLocation(left, right) {
|
|
41
|
+
return left.range.start.line - right.range.start.line
|
|
42
|
+
|| left.range.start.column - right.range.start.column;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareClassifications(left, right) {
|
|
46
|
+
return (CLASSIFICATION_ORDER.get(left) ?? 99) - (CLASSIFICATION_ORDER.get(right) ?? 99)
|
|
47
|
+
|| compareText(left, right);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function directRelations(graph, id) {
|
|
51
|
+
return {
|
|
52
|
+
authored_successors: (graph.incoming.get(id) ?? []).map((edge) => ({
|
|
53
|
+
id: edge.source,
|
|
54
|
+
authority_location: edge.sourceLocation,
|
|
55
|
+
relation_location: edge.relationLocation,
|
|
56
|
+
})),
|
|
57
|
+
authored_supersedes: (graph.outgoing.get(id) ?? []).map((edge) => ({
|
|
58
|
+
id: edge.target,
|
|
59
|
+
authority_location: edge.targetLocation,
|
|
60
|
+
relation_location: edge.relationLocation,
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function authorityDescriptor(graph, id) {
|
|
66
|
+
const entry = graph.byId.get(id);
|
|
67
|
+
if (entry === undefined) {
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
resolution: "missing",
|
|
71
|
+
lifecycle: null,
|
|
72
|
+
authority_location: null,
|
|
73
|
+
authored_successors: [],
|
|
74
|
+
authored_supersedes: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
resolution: "resolved",
|
|
80
|
+
lifecycle: entry.node.lifecycle,
|
|
81
|
+
authority_location: entry.node.location,
|
|
82
|
+
...directRelations(graph, id),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function classifyOccurrence(graph, occurrence) {
|
|
87
|
+
const descriptor = authorityDescriptor(graph, occurrence.authority_id);
|
|
88
|
+
const classifications = [];
|
|
89
|
+
if (descriptor.resolution === "missing") classifications.push("missing-reference");
|
|
90
|
+
else if (descriptor.lifecycle === "removed") classifications.push("removed-reference");
|
|
91
|
+
else classifications.push("active-reference");
|
|
92
|
+
if (descriptor.authored_successors.length > 0) classifications.push("superseded-reference");
|
|
93
|
+
if (occurrence.annotation_kind === "deprecated") classifications.push("explicit-deprecated");
|
|
94
|
+
return {
|
|
95
|
+
...occurrence,
|
|
96
|
+
classifications: classifications.sort(compareClassifications),
|
|
97
|
+
authority: descriptor,
|
|
98
|
+
pairing: occurrence.annotation_kind === "deprecated" ? "not_evaluated" : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findingFromOccurrence(occurrence, kind) {
|
|
103
|
+
return {
|
|
104
|
+
kind,
|
|
105
|
+
source_path: occurrence.source_path,
|
|
106
|
+
annotation_kind: occurrence.annotation_kind,
|
|
107
|
+
authority_id: occurrence.authority_id,
|
|
108
|
+
annotation_location: occurrence.annotation_location,
|
|
109
|
+
authority_location: occurrence.authority.authority_location,
|
|
110
|
+
authored_successors: occurrence.authority.authored_successors,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compareFindings(left, right) {
|
|
115
|
+
return compareText(left.source_path, right.source_path)
|
|
116
|
+
|| compareLocation(left.annotation_location, right.annotation_location)
|
|
117
|
+
|| compareText(left.kind, right.kind)
|
|
118
|
+
|| compareText(left.authority_id ?? "", right.authority_id ?? "");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function auditFindings(occurrences, invalidFindings) {
|
|
122
|
+
const findings = [...invalidFindings];
|
|
123
|
+
for (const occurrence of occurrences) {
|
|
124
|
+
for (const kind of occurrence.classifications) {
|
|
125
|
+
if (kind !== "active-reference") findings.push(findingFromOccurrence(occurrence, kind));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return findings.sort(compareFindings);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function countsFor(occurrences, invalidFindings) {
|
|
132
|
+
const counts = {
|
|
133
|
+
annotations: occurrences.length,
|
|
134
|
+
"active-reference": 0,
|
|
135
|
+
"explicit-deprecated": 0,
|
|
136
|
+
"removed-reference": 0,
|
|
137
|
+
"superseded-reference": 0,
|
|
138
|
+
"missing-reference": 0,
|
|
139
|
+
"invalid-annotation": invalidFindings.length,
|
|
140
|
+
};
|
|
141
|
+
for (const occurrence of occurrences) {
|
|
142
|
+
for (const classification of occurrence.classifications) counts[classification] += 1;
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeSourceSelector(source) {
|
|
148
|
+
if (
|
|
149
|
+
typeof source !== "string"
|
|
150
|
+
|| source.length === 0
|
|
151
|
+
|| source.includes("\\")
|
|
152
|
+
|| source.startsWith("/")
|
|
153
|
+
|| source.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
|
|
154
|
+
) return null;
|
|
155
|
+
return source;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readCurrentSource(repository, source, remainingBytes) {
|
|
159
|
+
const absolute = path.join(repository, ...source.split("/"));
|
|
160
|
+
let bytes;
|
|
161
|
+
try {
|
|
162
|
+
bytes = await captureStableRegularFile(absolute, source, repository, remainingBytes);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error?.code === "AUTH_REVIEW_INPUT_BUDGET") {
|
|
165
|
+
fail("CODE_AUTHORITY_INPUT_BUDGET", "max-bytes cannot contain the complete authority and source input", 1);
|
|
166
|
+
}
|
|
167
|
+
fail("CODE_AUTHORITY_SOURCE_INVALID", "tracked current source is not one stable repository-contained regular file");
|
|
168
|
+
}
|
|
169
|
+
let text;
|
|
170
|
+
try {
|
|
171
|
+
text = UTF8.decode(bytes);
|
|
172
|
+
} catch {
|
|
173
|
+
fail("CODE_AUTHORITY_SOURCE_INVALID", "tracked current source is not valid UTF-8");
|
|
174
|
+
}
|
|
175
|
+
return { bytes: bytes.length, text };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function queryCodeAuthority({ repository: repositoryPath, selector, maxFiles, maxBytes }) {
|
|
179
|
+
let inventory;
|
|
180
|
+
try {
|
|
181
|
+
inventory = await repositoryInventory(repositoryPath);
|
|
182
|
+
} catch {
|
|
183
|
+
fail("CODE_AUTHORITY_REPOSITORY_INVALID", "repo must name one exact readable Git worktree root");
|
|
184
|
+
}
|
|
185
|
+
let corpus;
|
|
186
|
+
try {
|
|
187
|
+
corpus = await resolveRepositoryCorpus(
|
|
188
|
+
inventory.repository,
|
|
189
|
+
path.join(inventory.repository, ".nimi", "spec"),
|
|
190
|
+
"code authority",
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
fail("CODE_AUTHORITY_CORPUS_INVALID", "repository canonical authority corpus is not readable");
|
|
194
|
+
}
|
|
195
|
+
let compiled;
|
|
196
|
+
try {
|
|
197
|
+
compiled = await compileAuthorityPath(corpus.path);
|
|
198
|
+
} catch {
|
|
199
|
+
fail("CODE_AUTHORITY_CORPUS_INVALID", "canonical authority corpus cannot be compiled");
|
|
200
|
+
}
|
|
201
|
+
if (!compiled.ok) fail("CODE_AUTHORITY_CORPUS_INVALID", "canonical authority corpus does not compile");
|
|
202
|
+
if (compiled.inputBytes > maxBytes) {
|
|
203
|
+
fail("CODE_AUTHORITY_INPUT_BUDGET", "max-bytes cannot contain the complete authority and source input", 1);
|
|
204
|
+
}
|
|
205
|
+
const contract = await loadAuthorityContract();
|
|
206
|
+
const identifier = new RegExp(contract.identifier_pattern, "u");
|
|
207
|
+
if (selector.kind === "authority" && !identifier.test(selector.id)) {
|
|
208
|
+
fail("CODE_USAGE", "authority selector must be one exact canonical authority ID");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tracked = new Set(inventory.files);
|
|
212
|
+
const gitlinks = new Set(inventory.gitlinks);
|
|
213
|
+
let sourcePaths;
|
|
214
|
+
if (selector.kind === "source") {
|
|
215
|
+
const normalized = normalizeSourceSelector(selector.path);
|
|
216
|
+
if (normalized === null || !SUPPORTED_SOURCE.test(normalized) || !tracked.has(normalized) || gitlinks.has(normalized)) {
|
|
217
|
+
fail("CODE_AUTHORITY_SOURCE_INVALID", "source selector must name one tracked .ts, .tsx, or .go file");
|
|
218
|
+
}
|
|
219
|
+
sourcePaths = [normalized];
|
|
220
|
+
} else {
|
|
221
|
+
sourcePaths = inventory.files.filter((source) => SUPPORTED_SOURCE.test(source) && !gitlinks.has(source));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const requiredFiles = compiled.fileCount + sourcePaths.length;
|
|
225
|
+
if (requiredFiles > maxFiles) {
|
|
226
|
+
fail("CODE_AUTHORITY_FILES_BUDGET", "max-files cannot contain the complete authority and source file set", 1);
|
|
227
|
+
}
|
|
228
|
+
const graph = buildAuthorityGraphSnapshot(compiled, inventory.repository, ["supersedes"]);
|
|
229
|
+
const occurrences = [];
|
|
230
|
+
const invalidFindings = [];
|
|
231
|
+
let inputBytes = compiled.inputBytes;
|
|
232
|
+
for (const source of sourcePaths) {
|
|
233
|
+
const current = await readCurrentSource(inventory.repository, source, maxBytes - inputBytes);
|
|
234
|
+
inputBytes += current.bytes;
|
|
235
|
+
const scanned = scanAuthorityAnnotations(source, current.text, contract.identifier_pattern);
|
|
236
|
+
occurrences.push(...scanned.occurrences.map((occurrence) => classifyOccurrence(graph, occurrence)));
|
|
237
|
+
invalidFindings.push(...scanned.findings);
|
|
238
|
+
}
|
|
239
|
+
occurrences.sort(compareOccurrences);
|
|
240
|
+
invalidFindings.sort(compareAnnotationFindings);
|
|
241
|
+
const findings = auditFindings(occurrences, invalidFindings);
|
|
242
|
+
const counts = countsFor(occurrences, invalidFindings);
|
|
243
|
+
const usage = {
|
|
244
|
+
authority_files: compiled.fileCount,
|
|
245
|
+
source_files: sourcePaths.length,
|
|
246
|
+
input_bytes: inputBytes,
|
|
247
|
+
max_files: maxFiles,
|
|
248
|
+
max_bytes: maxBytes,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (selector.kind === "authority") {
|
|
252
|
+
const selectedOccurrences = occurrences.filter((occurrence) => occurrence.authority_id === selector.id);
|
|
253
|
+
const selectedFindings = findings.filter((finding) => finding.authority_id === selector.id);
|
|
254
|
+
return {
|
|
255
|
+
operation: "authority-to-code",
|
|
256
|
+
scope: { repository: ".", authority_id: selector.id },
|
|
257
|
+
result: {
|
|
258
|
+
authority: authorityDescriptor(graph, selector.id),
|
|
259
|
+
occurrences: selectedOccurrences,
|
|
260
|
+
},
|
|
261
|
+
findings: selectedFindings,
|
|
262
|
+
counts: countsFor(selectedOccurrences, []),
|
|
263
|
+
usage,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (selector.kind === "source") {
|
|
267
|
+
return {
|
|
268
|
+
operation: "code-to-authority",
|
|
269
|
+
scope: { repository: ".", source_path: selector.path },
|
|
270
|
+
result: {
|
|
271
|
+
source_path: selector.path,
|
|
272
|
+
occurrences,
|
|
273
|
+
unannotated_semantics: "not_evaluated",
|
|
274
|
+
},
|
|
275
|
+
findings,
|
|
276
|
+
counts,
|
|
277
|
+
usage,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
operation: "lifecycle-audit",
|
|
282
|
+
scope: { repository: ".", source_languages: ["go", "typescript", "tsx"] },
|
|
283
|
+
result: {
|
|
284
|
+
conclusion: findings.length === 0
|
|
285
|
+
? "annotated implementation surface has no deterministic authority lifecycle residue"
|
|
286
|
+
: "annotated implementation surface contains deterministic authority lifecycle residue",
|
|
287
|
+
unannotated_semantics: "not_evaluated",
|
|
288
|
+
},
|
|
289
|
+
findings,
|
|
290
|
+
counts,
|
|
291
|
+
usage,
|
|
292
|
+
};
|
|
293
|
+
}
|