@nimiplatform/nimi-coding 0.4.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +92 -55
  4. package/README.zh-CN.md +93 -56
  5. package/cli/commands/authority.mjs +80 -125
  6. package/cli/commands/clear.mjs +3 -2
  7. package/cli/commands/code.mjs +225 -0
  8. package/cli/commands/start.mjs +3 -2
  9. package/cli/constants.mjs +2 -1
  10. package/cli/dev/check-authority-surface.mjs +17 -8
  11. package/cli/dev/check-product-conformance.mjs +4 -32
  12. package/cli/dev/run-cli-smoke.mjs +6 -0
  13. package/cli/help.mjs +8 -3
  14. package/cli/index.mjs +3 -2
  15. package/cli/lib/authority/audit.mjs +262 -185
  16. package/cli/lib/authority/change-candidates.mjs +807 -0
  17. package/cli/lib/authority/closed-sets.mjs +7 -8
  18. package/cli/lib/authority/compile.mjs +6 -24
  19. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  20. package/cli/lib/authority/consumers.mjs +69 -19
  21. package/cli/lib/authority/diagnostics.mjs +10 -2
  22. package/cli/lib/authority/diff.mjs +77 -54
  23. package/cli/lib/authority/format.mjs +4 -32
  24. package/cli/lib/authority/git-snapshot.mjs +34 -379
  25. package/cli/lib/authority/graph.mjs +10 -10
  26. package/cli/lib/authority/impact.mjs +17 -144
  27. package/cli/lib/authority/review.mjs +33 -43
  28. package/cli/lib/authority/sarif.mjs +1 -8
  29. package/cli/lib/authority/source-yaml.mjs +0 -4
  30. package/cli/lib/authority/surface.mjs +19 -1
  31. package/cli/lib/authority/terms.mjs +10 -9
  32. package/cli/lib/authority/validate.mjs +6 -14
  33. package/cli/lib/code/authority-annotation-response.mjs +43 -0
  34. package/cli/lib/code/authority-annotation-scan.mjs +162 -0
  35. package/cli/lib/code/authority-annotations.mjs +293 -0
  36. package/cli/lib/code/context.mjs +710 -0
  37. package/cli/lib/code/response.mjs +165 -0
  38. package/cli/lib/doctor.mjs +3 -2
  39. package/cli/lib/entrypoints.mjs +13 -5
  40. package/cli/seeds/seed-policy.yaml +2 -2
  41. package/contracts/authority-source.schema.yaml +0 -6
  42. package/contracts/authority-verifier-bindings.schema.yaml +21 -10
  43. package/methodology/authority-authoring.yaml +68 -70
  44. package/methodology/core.yaml +10 -6
  45. package/package.json +5 -3
  46. package/spec/authority-audit.yaml +56 -52
  47. package/spec/authority-authoring.yaml +3 -16
  48. package/spec/authority-change-candidates.yaml +146 -0
  49. package/spec/authority-consumers.yaml +7 -0
  50. package/spec/authority-discovery.yaml +2 -2
  51. package/spec/authority-graph.yaml +1 -1
  52. package/spec/authority-impact.yaml +16 -39
  53. package/spec/authority-review.yaml +29 -47
  54. package/spec/authority-terms.yaml +5 -1
  55. package/spec/code-authority-annotations.yaml +42 -0
  56. package/spec/code-context.yaml +38 -0
  57. package/spec/product-scope.yaml +15 -9
  58. package/cli/commands/validate-ai-governance.mjs +0 -335
  59. package/cli/lib/authority/evidence-snapshot.mjs +0 -1
  60. package/cli/lib/authority/evidence.mjs +0 -1432
  61. package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
  62. package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
  63. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
  64. package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
  65. package/cli/lib/internal/governance/config.mjs +0 -114
  66. package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
  67. package/contracts/authority-evidence-bindings.schema.yaml +0 -53
  68. package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
  69. package/contracts/authority-impact-dispositions.schema.yaml +0 -13
  70. package/spec/authority-evidence.yaml +0 -200
@@ -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
+ }