@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
@@ -65,11 +65,6 @@ function refused(diagnostics, { fileCount = 0 } = {}) {
65
65
  };
66
66
  }
67
67
 
68
- function isWithin(root, candidate) {
69
- const relative = path.relative(root, candidate);
70
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
71
- }
72
-
73
68
  function portableRelative(repository, candidate) {
74
69
  return path.relative(repository, candidate).split(path.sep).join(path.posix.sep);
75
70
  }
@@ -540,12 +535,16 @@ export async function closedSetsAuthorityRepository(repositoryPath, specPath, {
540
535
  }
541
536
  const canonical = canonicalGroups(compiled);
542
537
 
538
+ const gitlinks = new Set(inventory.gitlinks);
543
539
  const classifiedFiles = await classifyTrackedTextFiles(
544
540
  inventory.repository,
545
541
  inventory.files,
546
- (file) => file === ".nimi/spec" || file.startsWith(".nimi/spec/")
547
- ? "authority-corpus"
548
- : implementationFileKind(file) === null ? "not-a-scanned-kind" : null,
542
+ (file) => {
543
+ if (gitlinks.has(file)) return "gitlink";
544
+ return file === ".nimi/spec" || file.startsWith(".nimi/spec/")
545
+ ? "authority-corpus"
546
+ : implementationFileKind(file) === null ? "not-a-scanned-kind" : null;
547
+ },
549
548
  );
550
549
  if (classifiedFiles.error !== undefined) {
551
550
  return refused([inputDiagnostic("AUTH_CLOSED_SETS_IMPLEMENTATION_INPUT_INVALID", classifiedFiles.error)], { fileCount: compiled.fileCount });
@@ -1,4 +1,4 @@
1
- import { compareText, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
1
+ import { compareText } from "./diagnostics.mjs";
2
2
  import { formattingDiagnostics, parseAuthorityPath } from "./format.mjs";
3
3
  import { validateAuthorityScopeBindings } from "./scope-bindings.mjs";
4
4
  import { buildSourceMap } from "./source-map.mjs";
@@ -39,31 +39,13 @@ async function admittedSources(inputPath) {
39
39
  return { ok: true, diagnostics: [], parsed };
40
40
  }
41
41
 
42
- function requiredFormatDiagnostics(documents, requiredFormat) {
43
- if (requiredFormat === null) return [];
44
- return sortDiagnostics(documents.flatMap((document) => {
45
- const found = document.data?.format;
46
- if (found === requiredFormat) return [];
47
- return [makeDiagnostic({
48
- code: "AUTH_FORMAT_REQUIRED",
49
- file: document.file,
50
- range: document.locations.get("/format") ?? document.locations.get("") ?? document.locator.range(0),
51
- pointer: "/format",
52
- reason: `authority check requires format ${requiredFormat}, but ${document.file} declares ${found}`,
53
- repair: "rewrite the container in the explicitly required admitted format without changing product semantics",
54
- })];
55
- }));
56
- }
57
-
58
- export async function checkAuthorityPath(inputPath, { scopeBindings = null, requireFormat = null } = {}) {
42
+ export async function checkAuthorityPath(inputPath, { scopeBindings = null } = {}) {
59
43
  const result = await admittedSources(inputPath);
60
- const formatDiagnostics = requiredFormatDiagnostics(result.parsed.documents, requireFormat);
61
- const diagnostics = sortDiagnostics([...result.diagnostics, ...formatDiagnostics]);
62
- if (!result.ok || formatDiagnostics.length > 0 || scopeBindings === null) return {
63
- ok: result.ok && formatDiagnostics.length === 0,
64
- diagnostics,
44
+ if (!result.ok || scopeBindings === null) return {
45
+ ok: result.ok,
46
+ diagnostics: result.diagnostics,
65
47
  fileCount: result.parsed.files.length,
66
- unitCount: result.ok && formatDiagnostics.length === 0 ? result.parsed.sources.length : 0,
48
+ unitCount: result.ok ? result.parsed.sources.length : 0,
67
49
  };
68
50
  const scopeValidation = await validateAuthorityScopeBindings(result.parsed.sources, scopeBindings);
69
51
  return {
@@ -0,0 +1,143 @@
1
+ import path from "node:path";
2
+
3
+ import { compareText, portablePath } from "./diagnostics.mjs";
4
+
5
+ class ReadonlyIndex {
6
+ #values;
7
+
8
+ constructor(entries) {
9
+ this.#values = new Map(entries);
10
+ Object.freeze(this);
11
+ }
12
+
13
+ get size() {
14
+ return this.#values.size;
15
+ }
16
+
17
+ get(key) {
18
+ return this.#values.get(key);
19
+ }
20
+
21
+ has(key) {
22
+ return this.#values.has(key);
23
+ }
24
+
25
+ *entries() {
26
+ for (const [key, value] of this.#values) yield Object.freeze([key, value]);
27
+ }
28
+
29
+ keys() {
30
+ return this.#values.keys();
31
+ }
32
+
33
+ values() {
34
+ return this.#values.values();
35
+ }
36
+
37
+ forEach(callback, thisArg = undefined) {
38
+ for (const [key, value] of this.#values) callback.call(thisArg, value, key, this);
39
+ }
40
+
41
+ [Symbol.iterator]() {
42
+ return this.entries();
43
+ }
44
+
45
+ get [Symbol.toStringTag]() {
46
+ return "ReadonlyIndex";
47
+ }
48
+ }
49
+
50
+ function freezeTree(value, seen = new Set()) {
51
+ if (value === null || typeof value !== "object" || Object.isFrozen(value) || seen.has(value)) return value;
52
+ seen.add(value);
53
+ for (const key of Reflect.ownKeys(value)) freezeTree(value[key], seen);
54
+ return Object.freeze(value);
55
+ }
56
+
57
+ function readonlyIndex(entries) {
58
+ return new ReadonlyIndex([...entries].sort(([left], [right]) => compareText(left, right)));
59
+ }
60
+
61
+ function groupedIndex(entries, keys) {
62
+ const groups = new Map();
63
+ for (const entry of entries) {
64
+ for (const key of keys(entry)) {
65
+ const values = groups.get(key) ?? [];
66
+ values.push(entry);
67
+ groups.set(key, values);
68
+ }
69
+ }
70
+ return readonlyIndex([...groups.entries()].map(([key, values]) => [
71
+ key,
72
+ Object.freeze(values.sort((left, right) => compareText(left.unit.id, right.unit.id))),
73
+ ]));
74
+ }
75
+
76
+ function compareEdge(left, right) {
77
+ return compareText(left.source, right.source)
78
+ || compareText(left.type, right.type)
79
+ || compareText(left.target, right.target);
80
+ }
81
+
82
+ function adjacencyIndexes(entries) {
83
+ const outgoing = new Map(entries.map((entry) => [entry.unit.id, []]));
84
+ const incoming = new Map(entries.map((entry) => [entry.unit.id, []]));
85
+ for (const entry of entries) {
86
+ entry.unit.relations.forEach((relation, relationIndex) => {
87
+ const edge = Object.freeze({
88
+ source: entry.unit.id,
89
+ type: relation.type,
90
+ target: relation.target,
91
+ sourceUnitIndex: entry.unitIndex,
92
+ relationIndex,
93
+ });
94
+ outgoing.get(edge.source).push(edge);
95
+ incoming.get(edge.target).push(edge);
96
+ });
97
+ }
98
+ const finish = (index) => readonlyIndex([...index.entries()].map(([id, edges]) => [
99
+ id,
100
+ Object.freeze(edges.sort(compareEdge)),
101
+ ]));
102
+ return { outgoing: finish(outgoing), incoming: finish(incoming) };
103
+ }
104
+
105
+ export function createCompiledAuthoritySnapshot(compiled, inputRoot) {
106
+ if (
107
+ compiled?.ok !== true
108
+ || !Array.isArray(compiled?.ir?.units)
109
+ || compiled?.sourceMap?.fields === null
110
+ || typeof compiled?.sourceMap?.fields !== "object"
111
+ ) {
112
+ throw new TypeError("compiled authority snapshot requires one successful compiler result");
113
+ }
114
+ if (typeof inputRoot !== "string" || inputRoot.length === 0) {
115
+ throw new TypeError("compiled authority snapshot requires one non-empty input root");
116
+ }
117
+ const resolvedRoot = path.resolve(inputRoot);
118
+ const entries = compiled.ir.units.map((unit, unitIndex) => {
119
+ const mapped = compiled.sourceMap.fields[`/units/${unitIndex}/id`];
120
+ if (!mapped) throw new TypeError(`compiled authority snapshot cannot locate unit identity: ${unit.id}`);
121
+ return Object.freeze({
122
+ unit,
123
+ unitIndex,
124
+ container: portablePath(mapped.file, resolvedRoot),
125
+ });
126
+ });
127
+ const byId = readonlyIndex(entries.map((entry) => [entry.unit.id, entry]));
128
+ const byOwner = groupedIndex(entries, (entry) => [entry.unit.owner]);
129
+ const byScope = groupedIndex(entries, (entry) => entry.unit.semantic?.scope ?? []);
130
+ const byContainer = groupedIndex(entries, (entry) => [entry.container]);
131
+ const { incoming, outgoing } = adjacencyIndexes(entries);
132
+ freezeTree(compiled);
133
+ return Object.freeze({
134
+ inputRoot: resolvedRoot,
135
+ compiled,
136
+ byId,
137
+ byOwner,
138
+ byScope,
139
+ byContainer,
140
+ incoming,
141
+ outgoing,
142
+ });
143
+ }
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import YAML from "yaml";
5
5
 
6
- import { compareText, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
6
+ import { compareText, isPathWithin, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
7
7
  import { AuthorityReviewRefusal } from "./git-snapshot.mjs";
8
8
  import { resolveRepositoryCorpus } from "./repository-corpus.mjs";
9
9
  import { repositoryInventory } from "./repository-inventory.mjs";
@@ -15,7 +15,10 @@ const CONSUMERS_FORMAT = "nimicoding.authority-consumers/v1";
15
15
  // requiring a trailing segment reported it as no reference at all.
16
16
  // A separator run rather than one character: inside a source string literal a
17
17
  // Windows path is written escaped, so the file text holds ".nimi\\spec".
18
- const SPEC_REFERENCE = /\.nimi[/\\]+spec(?:[/\\]+[A-Za-z0-9_.\\/*<>-]*[A-Za-z0-9_*<>-])?/gu;
18
+ // A single backslash followed by a common source-string escape starter ends a
19
+ // local path instead of creating another path segment. Escaped Windows
20
+ // separators remain visible in source as a run of at least two backslashes.
21
+ const SPEC_REFERENCE = /\.nimi(?:\/+|\\{2,}|\\(?![nrtbfv0xu'"`$]))spec(?:(?:\/+|\\{2,}|\\(?![nrtbfv0xu'"`$]))[A-Za-z0-9_.*<>-]+)*/gu;
19
22
  // A path built from adjacent string literals, e.g. path.join(root, '.nimi',
20
23
  // 'spec', 'runtime', 'x.yaml'). Reconstructed only while every segment is a
21
24
  // literal; the first non-literal segment makes the target unresolved.
@@ -84,11 +87,6 @@ function repositoryDiagnostic(error) {
84
87
  return inputDiagnostic("AUTH_CONSUMERS_REPOSITORY_INVALID", error.message, error.file ?? ".");
85
88
  }
86
89
 
87
- function isWithin(root, candidate) {
88
- const relative = path.relative(root, candidate);
89
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
90
- }
91
-
92
90
  async function resolveCorpus(repository, specPath) {
93
91
  return resolveRepositoryCorpus(repository, specPath, "authority consumer");
94
92
  }
@@ -115,29 +113,66 @@ function lineCounter(text) {
115
113
  };
116
114
  }
117
115
 
116
+ function collapseEscapedLineContinuations(text) {
117
+ const chunks = [];
118
+ const continuations = [];
119
+ let segmentStart = 0;
120
+ let searchFrom = 0;
121
+ let normalizedLength = 0;
122
+ for (let index = text.indexOf("\\", searchFrom); index !== -1; index = text.indexOf("\\", searchFrom)) {
123
+ const width = text[index + 1] === "\n"
124
+ ? 2
125
+ : text[index + 1] === "\r" && text[index + 2] === "\n" ? 3 : 0;
126
+ if (width === 0) {
127
+ searchFrom = index + 1;
128
+ continue;
129
+ }
130
+ const chunk = text.slice(segmentStart, index);
131
+ chunks.push(chunk);
132
+ normalizedLength += chunk.length;
133
+ continuations.push({ offset: normalizedLength, width });
134
+ segmentStart = index + width;
135
+ searchFrom = segmentStart;
136
+ }
137
+ if (continuations.length === 0) return { text, originalOffset: (index) => index };
138
+ chunks.push(text.slice(segmentStart));
139
+ return {
140
+ text: chunks.join(""),
141
+ originalOffset(index) {
142
+ let removedWidth = 0;
143
+ for (const continuation of continuations) {
144
+ if (continuation.offset > index) break;
145
+ removedWidth += continuation.width;
146
+ }
147
+ return index + removedWidth;
148
+ },
149
+ };
150
+ }
151
+
118
152
  function extractReferences(consumer, text) {
153
+ const logical = collapseEscapedLineContinuations(text);
119
154
  const found = [];
120
- for (const match of text.matchAll(SPEC_REFERENCE)) {
155
+ for (const match of logical.text.matchAll(SPEC_REFERENCE)) {
121
156
  const target = match[0].replace(/[/\\]+/gu, "/");
122
157
  if (target.includes("*") || target.includes("<") || target.includes(">")) continue;
123
- found.push({ index: match.index, target, reason: null });
158
+ found.push({ index: logical.originalOffset(match.index), target, reason: null });
124
159
  }
125
- for (const match of text.matchAll(SPEC_CONSTRUCTOR)) {
160
+ for (const match of logical.text.matchAll(SPEC_CONSTRUCTOR)) {
126
161
  const segments = [...match[3].matchAll(CONSTRUCTOR_SEGMENT)].map((segment) => segment[2]);
127
162
  const target = [".nimi", "spec", ...segments].join("/");
128
163
  // A further segment that is not a string literal makes the built path
129
164
  // unknowable here. Reporting it as unresolved keeps the scan from
130
165
  // presenting silence as proof that nothing consumes the corpus.
131
- const continues = CONSTRUCTOR_CONTINUES.test(text.slice(match.index + match[0].length));
132
- found.push({ index: match.index, target, reason: continues ? "non-literal-path-segment" : null });
166
+ const continues = CONSTRUCTOR_CONTINUES.test(logical.text.slice(match.index + match[0].length));
167
+ found.push({ index: logical.originalOffset(match.index), target, reason: continues ? "non-literal-path-segment" : null });
133
168
  }
134
- for (const match of text.matchAll(EXTERNAL_REFERENCE)) {
169
+ for (const match of logical.text.matchAll(EXTERNAL_REFERENCE)) {
135
170
  const externalPath = match[2];
136
171
  const fragment = match[3] ?? null;
137
172
  const coordinate = `${match[1]}:${externalPath}${fragment === null ? "" : `#${fragment}`}`;
138
173
  const nonCanonical = externalPath.split("/").some((segment) => segment === "." || segment === "..");
139
174
  found.push({
140
- index: match.index + match[0].indexOf(`${match[1]}:`),
175
+ index: logical.originalOffset(match.index + match[0].indexOf(`${match[1]}:`)),
141
176
  target: coordinate,
142
177
  scheme: match[1],
143
178
  path: externalPath,
@@ -173,18 +208,28 @@ function targetExists(target, trackedFiles) {
173
208
  // own. Without a root for that scheme the target is unknowable here, which is
174
209
  // not the same claim as dead.
175
210
  function classifyLocalReference(reference, trackedFiles) {
176
- return targetExists(reference.target, trackedFiles) ? "canonical" : "canonicalDead";
211
+ if (targetExists(reference.target, trackedFiles)) return { classification: "canonical", reference };
212
+ if (reference.target.endsWith(".")) {
213
+ const withoutSentencePeriod = reference.target.slice(0, -1);
214
+ if (targetExists(withoutSentencePeriod, trackedFiles)) {
215
+ return {
216
+ classification: "canonical",
217
+ reference: { ...reference, target: withoutSentencePeriod },
218
+ };
219
+ }
220
+ }
221
+ return { classification: "canonicalDead", reference };
177
222
  }
178
223
 
179
224
  async function externalTargetStatus(root, relative) {
180
225
  const candidate = path.resolve(root, ...relative.split("/"));
181
- if (!isWithin(root, candidate)) return "non-canonical";
226
+ if (!isPathWithin(root, candidate)) return "non-canonical";
182
227
 
183
228
  let probe = candidate;
184
229
  while (true) {
185
230
  try {
186
231
  const resolved = await realpath(probe);
187
- if (!isWithin(root, resolved)) return "non-canonical";
232
+ if (!isPathWithin(root, resolved)) return "non-canonical";
188
233
  return probe === candidate ? "authority" : "dead";
189
234
  } catch (error) {
190
235
  if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
@@ -354,10 +399,14 @@ export async function consumersAuthorityRepository(repositoryPath, specPath, { m
354
399
  }
355
400
 
356
401
  const candidates = [...new Set([...inventory.files, "package.json"])].sort(compareText);
402
+ const gitlinks = new Set(inventory.gitlinks);
357
403
  const classifiedFiles = await classifyTrackedTextFiles(
358
404
  inventory.repository,
359
405
  candidates,
360
- (file) => isSpecSource(file) ? "authority-corpus" : null,
406
+ (file) => {
407
+ if (gitlinks.has(file)) return "gitlink";
408
+ return isSpecSource(file) ? "authority-corpus" : null;
409
+ },
361
410
  );
362
411
  if (classifiedFiles.error !== undefined) {
363
412
  return refused([inputDiagnostic("AUTH_CONSUMERS_INPUT_INVALID", classifiedFiles.error)]);
@@ -401,7 +450,8 @@ export async function consumersAuthorityRepository(repositoryPath, specPath, { m
401
450
  try {
402
451
  for (const reference of references) {
403
452
  if (reference.external === undefined) {
404
- classified[classifyLocalReference(reference, trackedFiles)].push(reference);
453
+ const local = classifyLocalReference(reference, trackedFiles);
454
+ classified[local.classification].push(local.reference);
405
455
  continue;
406
456
  }
407
457
  const root = roots.get(reference.external.scheme);
@@ -1,14 +1,22 @@
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);
6
8
  return a === b ? 0 : a < b ? -1 : 1;
7
9
  }
8
10
 
11
+ export function isPathWithin(root, candidate) {
12
+ const relative = path.relative(root, candidate);
13
+ return relative === ""
14
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
15
+ }
16
+
9
17
  export function portablePath(filePath, cwd = process.cwd()) {
10
18
  const relative = path.relative(cwd, filePath);
11
- const value = relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : filePath;
19
+ const value = relative && isPathWithin(cwd, filePath) ? relative : filePath;
12
20
  return value.split(path.sep).join(path.posix.sep);
13
21
  }
14
22
 
@@ -83,5 +91,5 @@ export const REPAIRS = {
83
91
  invalid: "replace the value with one admitted by the authoring contract",
84
92
  relation: "repair the explicit relation using product authority; do not infer a target",
85
93
  duplicate: "keep one declaration chosen from product authority and remove the duplicate",
86
- format: "run `nimicoding authority fmt` on this file",
94
+ format: `run \`${PROJECT_LOCAL_CLI_INVOCATION} authority fmt\` on this file`,
87
95
  };