@nimiplatform/nimi-coding 0.3.1 → 0.5.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 (115) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/README.md +297 -41
  4. package/README.zh-CN.md +306 -31
  5. package/bin/nimicoding.mjs +1 -1
  6. package/cli/commands/authority.mjs +804 -0
  7. package/cli/commands/clear.mjs +27 -7
  8. package/cli/commands/doctor.mjs +10 -1
  9. package/cli/commands/start.mjs +23 -27
  10. package/cli/commands/sync.mjs +21 -101
  11. package/cli/constants.mjs +7 -31
  12. package/cli/dev/check-authority-surface.mjs +182 -0
  13. package/cli/dev/check-product-conformance.mjs +889 -0
  14. package/cli/help.mjs +10 -17
  15. package/cli/index.mjs +6 -47
  16. package/cli/lib/authority/anchors.mjs +491 -0
  17. package/cli/lib/authority/audit.mjs +567 -0
  18. package/cli/lib/authority/change-candidates.mjs +807 -0
  19. package/cli/lib/authority/closed-sets.mjs +645 -0
  20. package/cli/lib/authority/compile.mjs +92 -0
  21. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  22. package/cli/lib/authority/consumers.mjs +520 -0
  23. package/cli/lib/authority/diagnostics.mjs +93 -0
  24. package/cli/lib/authority/diff.mjs +310 -0
  25. package/cli/lib/authority/discover.mjs +447 -0
  26. package/cli/lib/authority/format.mjs +169 -0
  27. package/cli/lib/authority/git-snapshot.mjs +720 -0
  28. package/cli/lib/authority/graph.mjs +351 -0
  29. package/cli/lib/authority/impact.mjs +125 -0
  30. package/cli/lib/authority/lexicon.mjs +55 -0
  31. package/cli/lib/authority/query.mjs +162 -0
  32. package/cli/lib/authority/repository-corpus.mjs +37 -0
  33. package/cli/lib/authority/repository-inventory.mjs +34 -0
  34. package/cli/lib/authority/review.mjs +212 -0
  35. package/cli/lib/authority/sarif.mjs +174 -0
  36. package/cli/lib/authority/scope-bindings.mjs +225 -0
  37. package/cli/lib/authority/source-map.mjs +105 -0
  38. package/cli/lib/authority/source-markdown.mjs +167 -0
  39. package/cli/lib/authority/source-yaml.mjs +308 -0
  40. package/cli/lib/authority/surface.mjs +97 -0
  41. package/cli/lib/authority/terms.mjs +422 -0
  42. package/cli/lib/authority/tracked-text.mjs +70 -0
  43. package/cli/lib/authority/validate.mjs +300 -0
  44. package/cli/lib/bootstrap.mjs +55 -71
  45. package/cli/lib/doctor.mjs +40 -123
  46. package/cli/lib/entrypoints.mjs +104 -105
  47. package/cli/lib/fs-helpers.mjs +139 -15
  48. package/cli/lib/internal/governance/config.mjs +0 -36
  49. package/cli/lib/shared.mjs +8 -21
  50. package/cli/lib/sync.mjs +81 -92
  51. package/cli/lib/value-helpers.mjs +0 -26
  52. package/cli/lib/yaml-helpers.mjs +1 -124
  53. package/cli/seeds/bootstrap.mjs +31 -87
  54. package/cli/seeds/seed-policy.yaml +9 -63
  55. package/contracts/authority-scope-bindings.schema.yaml +15 -0
  56. package/contracts/authority-source.schema.yaml +58 -0
  57. package/contracts/authority-verifier-bindings.schema.yaml +31 -0
  58. package/methodology/authority-authoring.yaml +127 -0
  59. package/methodology/core.yaml +34 -27
  60. package/package.json +10 -6
  61. package/spec/authority-anchors.yaml +99 -0
  62. package/spec/authority-audit.yaml +89 -0
  63. package/spec/authority-authoring.yaml +137 -0
  64. package/spec/authority-change-candidates.yaml +146 -0
  65. package/spec/authority-closed-sets.yaml +65 -0
  66. package/spec/authority-consumers.yaml +77 -0
  67. package/spec/authority-context.yaml +50 -0
  68. package/spec/authority-discovery.yaml +119 -0
  69. package/spec/authority-graph.yaml +59 -0
  70. package/spec/authority-impact.yaml +61 -0
  71. package/spec/authority-review.yaml +71 -0
  72. package/spec/authority-terms.yaml +63 -0
  73. package/spec/product-scope.yaml +37 -23
  74. package/cli/commands/blueprint-audit.mjs +0 -91
  75. package/cli/commands/classify-spec-tree.mjs +0 -5
  76. package/cli/commands/generate-spec-derived-docs.mjs +0 -151
  77. package/cli/commands/generate-spec-migration-plan.mjs +0 -30
  78. package/cli/commands/surface-validator-command.mjs +0 -49
  79. package/cli/commands/validate-domain-admission.mjs +0 -5
  80. package/cli/commands/validate-guidance-bodies.mjs +0 -5
  81. package/cli/commands/validate-placement.mjs +0 -5
  82. package/cli/commands/validate-projection-edges.mjs +0 -5
  83. package/cli/commands/validate-spec-audit.mjs +0 -27
  84. package/cli/commands/validate-spec-governance.mjs +0 -144
  85. package/cli/commands/validate-spec-tree.mjs +0 -27
  86. package/cli/commands/validate-table-family.mjs +0 -5
  87. package/cli/commands/validate-tracked-output-admission.mjs +0 -5
  88. package/cli/lib/blueprint-audit.mjs +0 -363
  89. package/cli/lib/contracts.mjs +0 -21
  90. package/cli/lib/internal/contracts-loaders.mjs +0 -47
  91. package/cli/lib/internal/contracts-parse.mjs +0 -192
  92. package/cli/lib/internal/governance/runner.mjs +0 -35
  93. package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
  94. package/cli/lib/internal/validators-shared.mjs +0 -12
  95. package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
  96. package/cli/lib/internal/validators-spec.mjs +0 -380
  97. package/cli/lib/validators.mjs +0 -30
  98. package/config/bootstrap.yaml +0 -6
  99. package/config/spec-generation-inputs.yaml +0 -41
  100. package/contracts/domain-admission.schema.yaml +0 -56
  101. package/contracts/migration-inventory.schema.yaml +0 -15
  102. package/contracts/negative-fixtures.yaml +0 -26
  103. package/contracts/placement-contract.schema.yaml +0 -127
  104. package/contracts/projection-edge.schema.yaml +0 -46
  105. package/contracts/shared-enums.yaml +0 -63
  106. package/contracts/spec-generation-audit.schema.yaml +0 -46
  107. package/contracts/spec-generation-inputs.schema.yaml +0 -43
  108. package/contracts/spec-layout.schema.yaml +0 -32
  109. package/contracts/surface-taxonomy.schema.yaml +0 -139
  110. package/contracts/table-family.schema.yaml +0 -133
  111. package/contracts/tracked-output-admission.schema.yaml +0 -17
  112. package/methodology/four-closure-policy.yaml +0 -20
  113. package/methodology/role-separation-policy.yaml +0 -22
  114. package/methodology/spec-reconstruction.yaml +0 -69
  115. package/spec/_meta/spec-tree-model.yaml +0 -44
package/cli/help.mjs CHANGED
@@ -1,28 +1,19 @@
1
1
  import { VERSION } from "./constants.mjs";
2
+ import { AUTHORITY_ANCHOR_GRAMMAR_HELP } from "./lib/authority/anchors.mjs";
3
+ import { AUTHORITY_COMMAND_USAGE } from "./commands/authority.mjs";
2
4
  import { localize, styleCommand, styleHeading, styleMuted } from "./lib/ui.mjs";
3
5
 
4
6
  export function helpText() {
5
7
  const commands = [
8
+ // The authority list comes from the command module itself so help and the
9
+ // parser cannot describe different surfaces.
10
+ ...AUTHORITY_COMMAND_USAGE,
6
11
  "nimicoding start [--yes]",
7
12
  "nimicoding sync [--apply|--check|--dry-run] [--json]",
8
13
  "nimicoding clear [--yes]",
9
14
  "nimicoding doctor [--verbose|--json]",
10
- "nimicoding blueprint-audit [--blueprint-root <path>] [--canonical-root <path>] [--json] [--write-local]",
11
- "nimicoding validate-spec-tree [.nimi/spec]",
12
- "nimicoding validate-spec-audit [.nimi/local/state/spec-generation/spec-generation-audit.yaml]",
13
- "nimicoding classify-spec-tree --profile <profile-id> --root .nimi/spec [--emit <path>] [--json]",
14
- "nimicoding generate-spec-migration-plan --profile <profile-id> --root .nimi/spec --emit .nimi/local/state/spec-surface/migration-plan.json [--json]",
15
- "nimicoding validate-placement --profile <profile-id> --root .nimi/spec [--json]",
16
- "nimicoding validate-table-family --profile <profile-id> --root .nimi/spec [--json]",
17
- "nimicoding validate-projection-edges --profile <profile-id> --root .nimi/spec [--json]",
18
- "nimicoding validate-guidance-bodies --profile <profile-id> --root .nimi/spec [--json]",
19
- "nimicoding validate-domain-admission --profile <profile-id> --root .nimi/spec [--json]",
20
- "nimicoding validate-tracked-output-admission --profile <profile-id> --root .nimi/spec [--json]",
21
- "nimicoding validate-spec-governance --profile <profile-id> --scope <all|host-defined-scope>",
22
- "nimicoding generate-spec-derived-docs --profile <profile-id> --scope <all|host-defined-scope> [--check]",
23
- "nimicoding validate-ai-governance --profile <profile-id> --scope <all|agents-freshness|context-budget|structure-budget|high-risk-doc-metadata>",
15
+ "nimicoding validate-ai-governance --profile <profile-id> --scope <all|agents-freshness|context-budget|structure-budget|high-risk-doc-metadata> # optional L3 repository governance",
24
16
  ];
25
-
26
17
  return [
27
18
  styleHeading(`nimicoding ${VERSION}`),
28
19
  "",
@@ -31,9 +22,11 @@ export function helpText() {
31
22
  ` ${styleCommand("nimicoding --version")}`,
32
23
  ...commands.map((command) => ` ${styleCommand(command)}`),
33
24
  "",
25
+ AUTHORITY_ANCHOR_GRAMMAR_HELP,
26
+ "",
34
27
  styleMuted(localize(
35
- "Nimi Coding defines methodology, canonical spec construction, and deterministic governance checks. AI task planning and execution belong to the host.",
36
- "Nimi Coding 只定义方法论、canonical spec 构建和确定性治理校验;AI 任务规划与执行由宿主负责。",
28
+ "Nimi Coding provides canonical authority methodology, formatting, compiler primitives, and deterministic gates. The optional L3 repository-governance command is not authority admission. AI task planning and execution belong to the host.",
29
+ "Nimi Coding 提供 canonical authority 方法论、格式化、编译器原语与确定性 gate。可选 L3 repository-governance 命令不属于 authority admission;AI 任务规划与执行由宿主负责。",
37
30
  )),
38
31
  "",
39
32
  ].join("\n");
package/cli/index.mjs CHANGED
@@ -1,43 +1,19 @@
1
- import { runBlueprintAudit } from "./commands/blueprint-audit.mjs";
2
- import { runClassifySpecTree } from "./commands/classify-spec-tree.mjs";
1
+ import { runAuthority } from "./commands/authority.mjs";
3
2
  import { runClear } from "./commands/clear.mjs";
4
3
  import { runDoctor } from "./commands/doctor.mjs";
5
- import { runGenerateSpecDerivedDocs } from "./commands/generate-spec-derived-docs.mjs";
6
- import { runGenerateSpecMigrationPlan } from "./commands/generate-spec-migration-plan.mjs";
7
4
  import { runStart } from "./commands/start.mjs";
8
5
  import { runSync } from "./commands/sync.mjs";
9
6
  import { runValidateAiGovernance } from "./commands/validate-ai-governance.mjs";
10
- import { runValidateDomainAdmission } from "./commands/validate-domain-admission.mjs";
11
- import { runValidateGuidanceBodies } from "./commands/validate-guidance-bodies.mjs";
12
- import { runValidatePlacement } from "./commands/validate-placement.mjs";
13
- import { runValidateProjectionEdges } from "./commands/validate-projection-edges.mjs";
14
- import { runValidateSpecAudit } from "./commands/validate-spec-audit.mjs";
15
- import { runValidateSpecGovernance } from "./commands/validate-spec-governance.mjs";
16
- import { runValidateSpecTree } from "./commands/validate-spec-tree.mjs";
17
- import { runValidateTableFamily } from "./commands/validate-table-family.mjs";
18
- import { runValidateTrackedOutputAdmission } from "./commands/validate-tracked-output-admission.mjs";
19
7
  import { helpText } from "./help.mjs";
20
8
  import { VERSION } from "./constants.mjs";
21
9
  import { configureCliUi, localize, parseGlobalUiOptions } from "./lib/ui.mjs";
22
10
 
23
11
  const COMMANDS = {
12
+ authority: runAuthority,
24
13
  start: runStart,
25
14
  sync: runSync,
26
15
  clear: runClear,
27
16
  doctor: runDoctor,
28
- "blueprint-audit": runBlueprintAudit,
29
- "validate-spec-governance": runValidateSpecGovernance,
30
- "validate-spec-audit": runValidateSpecAudit,
31
- "validate-spec-tree": runValidateSpecTree,
32
- "classify-spec-tree": runClassifySpecTree,
33
- "validate-placement": runValidatePlacement,
34
- "validate-table-family": runValidateTableFamily,
35
- "validate-projection-edges": runValidateProjectionEdges,
36
- "validate-guidance-bodies": runValidateGuidanceBodies,
37
- "validate-domain-admission": runValidateDomainAdmission,
38
- "validate-tracked-output-admission": runValidateTrackedOutputAdmission,
39
- "generate-spec-derived-docs": runGenerateSpecDerivedDocs,
40
- "generate-spec-migration-plan": runGenerateSpecMigrationPlan,
41
17
  "validate-ai-governance": runValidateAiGovernance,
42
18
  };
43
19
 
@@ -47,47 +23,30 @@ export async function runCli(args) {
47
23
  process.stderr.write(parsedUi.error);
48
24
  return 2;
49
25
  }
50
-
51
- configureCliUi({
52
- locale: parsedUi.locale,
53
- colorEnabled: parsedUi.colorEnabled,
54
- });
55
-
26
+ configureCliUi({ locale: parsedUi.locale, colorEnabled: parsedUi.colorEnabled });
56
27
  const [command] = parsedUi.args;
57
28
  const rest = parsedUi.args.slice(1);
58
29
 
59
30
  if (!command || command === "--help" || command === "-h" || command === "help") {
60
31
  if (rest.length > 0) {
61
- process.stderr.write(localize(
62
- `nimicoding help refused: unexpected arguments: ${rest.join(" ")}\n`,
63
- `nimicoding help 拒绝执行:存在未预期参数:${rest.join(" ")}\n`,
64
- ));
32
+ process.stderr.write(localize(`nimicoding help refused: unexpected arguments: ${rest.join(" ")}\n`, `nimicoding help 拒绝执行:存在未预期参数:${rest.join(" ")}\n`));
65
33
  return 2;
66
34
  }
67
35
  process.stdout.write(helpText());
68
36
  return 0;
69
37
  }
70
-
71
38
  if (command === "--version" || command === "-v" || command === "version") {
72
39
  if (rest.length > 0) {
73
- process.stderr.write(localize(
74
- `nimicoding version refused: unexpected arguments: ${rest.join(" ")}\n`,
75
- `nimicoding version 拒绝执行:存在未预期参数:${rest.join(" ")}\n`,
76
- ));
40
+ process.stderr.write(localize(`nimicoding version refused: unexpected arguments: ${rest.join(" ")}\n`, `nimicoding version 拒绝执行:存在未预期参数:${rest.join(" ")}\n`));
77
41
  return 2;
78
42
  }
79
43
  process.stdout.write(`${VERSION}\n`);
80
44
  return 0;
81
45
  }
82
-
83
46
  const runner = COMMANDS[command];
84
47
  if (!runner) {
85
- process.stderr.write(localize(
86
- `Unknown command: ${command}\n\n${helpText()}`,
87
- `未知命令:${command}\n\n${helpText()}`,
88
- ));
48
+ process.stderr.write(localize(`Unknown command: ${command}\n\n${helpText()}`, `未知命令:${command}\n\n${helpText()}`));
89
49
  return 2;
90
50
  }
91
-
92
51
  return runner(rest);
93
52
  }
@@ -0,0 +1,491 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { compileAuthorityPath } from "./compile.mjs";
6
+ import { compareText, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
7
+ import { AuthorityInputError } from "./format.mjs";
8
+ import { AuthorityReviewRefusal } from "./git-snapshot.mjs";
9
+ import {
10
+ CODE_EXTENSIONS,
11
+ SEGMENT_PATTERN,
12
+ continuesPathToken,
13
+ continuesPathTokenAfter,
14
+ normativeFields,
15
+ } from "./lexicon.mjs";
16
+ import { resolveRepositoryCorpus } from "./repository-corpus.mjs";
17
+ import { repositoryInventory } from "./repository-inventory.mjs";
18
+ import { loadScopeBindings } from "./scope-bindings.mjs";
19
+
20
+ const ANCHORS_FORMAT = "nimicoding.authority-anchors/v1";
21
+ const SCRIPT_NAME = /^[a-z][a-z0-9:-]*$/;
22
+ export const PNPM_BUILTIN_SUBCOMMANDS = new Set([
23
+ "add", "audit", "create", "dlx", "exec", "install", "link", "list",
24
+ "outdated", "patch", "publish", "remove", "run", "update", "why",
25
+ ]);
26
+ const NON_WHITESPACE = /\S+/gu;
27
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
28
+
29
+ export const AUTHORITY_ANCHOR_GRAMMAR_HELP = [
30
+ "Authority anchors use a closed lexical grammar over active statement, condition, failure, and meaning fields:",
31
+ ` A: a maximal non-whitespace token containing "/" and ending exactly in .<${CODE_EXTENSIONS.join("|")}>.`,
32
+ ` A excludes tokens beginning with "~" (including "~/"), "/", an ASCII-letter Windows drive prefix such as "C:\\" or "C:/", or "\\\\" (UNC).`,
33
+ " B: pnpm, one ASCII space, and a maximal non-whitespace script-name token; the name must contain ':' or fully match [a-z][a-z0-9:-]*.",
34
+ ` B excludes pnpm builtin subcommands: ${[...PNPM_BUILTIN_SUBCOMMANDS].join(", ")}.`,
35
+ ` C: with --embedded-paths, maximal embedded <root>/<segment>(/<segment>)*.<${CODE_EXTENSIONS.join("|")}> substrings, where segment is [A-Za-z0-9_.-]+ and roots come from Git tracked top-level segments plus .nimi.`,
36
+ "Admitted tokens are checked exactly without path normalization or command execution.",
37
+ ].join("\n");
38
+
39
+ function validBudget(value) {
40
+ return Number.isSafeInteger(value) && value > 0;
41
+ }
42
+
43
+ function baseDiagnostic(code, reason, repair, file = ".", range = null, pointer = "") {
44
+ return makeDiagnostic({
45
+ code,
46
+ file,
47
+ range: range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
48
+ pointer,
49
+ reason,
50
+ repair,
51
+ });
52
+ }
53
+
54
+ function budgetDiagnostic(kind, required, admitted, mapped = null) {
55
+ return baseDiagnostic(
56
+ "AUTH_ANCHOR_BUDGET",
57
+ `complete authority anchor validation requires ${required} ${kind} but ${kind === "UTF-8 bytes" ? "max-bytes" : kind === "active units" ? "max-units" : "max-anchors"} is ${admitted}`,
58
+ "increase the explicit anchor budget; partial lexical anchor validation is forbidden",
59
+ mapped?.file ?? ".",
60
+ mapped?.range,
61
+ mapped?.sourcePointer ?? "",
62
+ );
63
+ }
64
+
65
+ function refused(diagnostics, { fileCount = 0, anchorsBytes = 0 } = {}) {
66
+ return {
67
+ ok: false,
68
+ complete: false,
69
+ partial: false,
70
+ diagnostics: sortDiagnostics(diagnostics),
71
+ fileCount,
72
+ unitCount: 0,
73
+ anchorsChecked: 0,
74
+ anchorsBytes,
75
+ };
76
+ }
77
+
78
+ function fieldLocation(compiled, unitIndex, field) {
79
+ return compiled.sourceMap.fields[`/units/${unitIndex}/semantic/${field}`] ?? null;
80
+ }
81
+
82
+ function unitLocation(compiled, unitIndex) {
83
+ return compiled.sourceMap.fields[`/units/${unitIndex}/id`] ?? null;
84
+ }
85
+
86
+ function tokenRecords(text) {
87
+ return [...text.matchAll(NON_WHITESPACE)].map((match) => ({ text: match[0], start: match.index, end: match.index + match[0].length }));
88
+ }
89
+
90
+ function hasNonRepositoryPathPrefix(token) {
91
+ const firstCodeUnit = token.charCodeAt(0);
92
+ const hasAsciiDrivePrefix = token.length >= 3
93
+ && ((firstCodeUnit >= 65 && firstCodeUnit <= 90) || (firstCodeUnit >= 97 && firstCodeUnit <= 122))
94
+ && token[1] === ":"
95
+ && (token[2] === "/" || token[2] === "\\");
96
+ return token.startsWith("~")
97
+ || token.startsWith("/")
98
+ || token.startsWith("\\\\")
99
+ || hasAsciiDrivePrefix;
100
+ }
101
+
102
+ function isPathAnchor(token) {
103
+ return !hasNonRepositoryPathPrefix(token)
104
+ && !token.includes("<")
105
+ && !token.includes(">")
106
+ && token.includes("/")
107
+ && CODE_EXTENSIONS.some((extension) => token.endsWith(`.${extension}`));
108
+ }
109
+
110
+ export function extractLexicalAnchors(text) {
111
+ const tokens = tokenRecords(text);
112
+ const anchors = [];
113
+ for (const token of tokens) {
114
+ if (isPathAnchor(token.text)) anchors.push({ type: "path", text: token.text });
115
+ }
116
+ for (let index = 0; index + 1 < tokens.length; index += 1) {
117
+ const command = tokens[index];
118
+ const script = tokens[index + 1];
119
+ if (command.text !== "pnpm" || text.slice(command.end, script.start) !== " ") continue;
120
+ if (PNPM_BUILTIN_SUBCOMMANDS.has(script.text)) continue;
121
+ if (!script.text.includes(":") && !SCRIPT_NAME.test(script.text)) continue;
122
+ anchors.push({ type: "script", text: `pnpm ${script.text}`, script: script.text });
123
+ }
124
+ return anchors;
125
+ }
126
+
127
+ function compareAnchor(left, right) {
128
+ return compareText(left.unitId, right.unitId)
129
+ || compareText(left.text, right.text)
130
+ || compareText(left.field, right.field)
131
+ || compareText(left.type, right.type);
132
+ }
133
+
134
+ function escapeRegularExpression(text) {
135
+ return text.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
136
+ }
137
+
138
+ function repositoryPathRoots(trackedFiles) {
139
+ const roots = new Set([".nimi"]);
140
+ for (const file of trackedFiles) {
141
+ const separator = file.indexOf("/");
142
+ const root = separator === -1 ? file : file.slice(0, separator);
143
+ if (root.length > 0) roots.add(root);
144
+ }
145
+ return [...roots].sort((left, right) => right.length - left.length || compareText(left, right));
146
+ }
147
+
148
+ function hasExcludedEmbeddedPathCharacter(token, start, end) {
149
+ const pathLike = /[A-Za-z0-9_./*<>-]/u;
150
+ let contextStart = start;
151
+ let contextEnd = end;
152
+ while (contextStart > 0 && pathLike.test(token[contextStart - 1])) contextStart -= 1;
153
+ while (contextEnd < token.length && pathLike.test(token[contextEnd])) contextEnd += 1;
154
+ const context = token.slice(contextStart, contextEnd);
155
+ return context.includes("*") || context.includes("<") || context.includes(">");
156
+ }
157
+
158
+ function extractEmbeddedPathAnchors(text, roots) {
159
+ const rootExpression = roots.map(escapeRegularExpression).join("|");
160
+ if (rootExpression.length === 0) return [];
161
+ const segment = SEGMENT_PATTERN;
162
+ const extensions = [...CODE_EXTENSIONS].sort((left, right) => right.length - left.length || compareText(left, right));
163
+ const expression = new RegExp(
164
+ `(?:${rootExpression})/${segment}(?:/${segment})*\\.(?:${extensions.join("|")})`,
165
+ "gu",
166
+ );
167
+ const anchors = [];
168
+ for (const token of tokenRecords(text)) {
169
+ if (isPathAnchor(token.text)) continue;
170
+ for (const match of token.text.matchAll(expression)) {
171
+ // The boundary test must use the same alphabet the segment pattern
172
+ // admits. Testing only [A-Za-z0-9] let "retired-src/live.ts" report a
173
+ // live "src/live.ts" that no file was ever named, while treating every
174
+ // leading separator as a longer path hid "./src/dead.ts" entirely.
175
+ if (continuesPathToken(token.text, match.index)) continue;
176
+ if (continuesPathTokenAfter(token.text, match.index + match[0].length)) continue;
177
+ if (hasExcludedEmbeddedPathCharacter(token.text, match.index, match.index + match[0].length)) continue;
178
+ anchors.push({ type: "embedded-path", text: match[0] });
179
+ }
180
+ }
181
+ return anchors;
182
+ }
183
+
184
+ function extractCorpusAnchors(compiled) {
185
+ const active = [];
186
+ const anchors = [];
187
+ compiled.ir.units.forEach((unit, unitIndex) => {
188
+ if (unit.lifecycle !== "active") return;
189
+ active.push({ unit, unitIndex });
190
+ const fields = normativeFields(unit.kind);
191
+ for (const field of fields) {
192
+ for (const anchor of extractLexicalAnchors(unit.semantic[field])) {
193
+ anchors.push({
194
+ ...anchor,
195
+ unitId: unit.id,
196
+ field,
197
+ location: fieldLocation(compiled, unitIndex, field),
198
+ });
199
+ }
200
+ }
201
+ });
202
+ anchors.sort(compareAnchor);
203
+ return { active, anchors };
204
+ }
205
+
206
+ function extractCorpusEmbeddedPathAnchors(compiled, trackedFiles) {
207
+ const anchors = [];
208
+ const roots = repositoryPathRoots(trackedFiles);
209
+ compiled.ir.units.forEach((unit, unitIndex) => {
210
+ if (unit.lifecycle !== "active") return;
211
+ const fields = normativeFields(unit.kind);
212
+ for (const field of fields) {
213
+ for (const anchor of extractEmbeddedPathAnchors(unit.semantic[field], roots)) {
214
+ anchors.push({
215
+ ...anchor,
216
+ unitId: unit.id,
217
+ field,
218
+ location: fieldLocation(compiled, unitIndex, field),
219
+ });
220
+ }
221
+ }
222
+ });
223
+ return anchors;
224
+ }
225
+
226
+ function anchorDiagnostic(anchor, code, reason, repair) {
227
+ return {
228
+ ...baseDiagnostic(
229
+ code,
230
+ reason,
231
+ repair,
232
+ anchor.location?.file ?? ".",
233
+ anchor.location?.range,
234
+ anchor.location?.sourcePointer ?? "",
235
+ ),
236
+ unitId: anchor.unitId,
237
+ field: anchor.field,
238
+ anchor: anchor.text,
239
+ };
240
+ }
241
+
242
+ function globRegularExpression(glob) {
243
+ let expression = "^";
244
+ for (let index = 0; index < glob.length; index += 1) {
245
+ const character = glob[index];
246
+ if (character === "*" && glob[index + 1] === "*") {
247
+ if (glob[index + 2] === "/") {
248
+ expression += "(?:.*/)?";
249
+ index += 2;
250
+ } else {
251
+ expression += ".*";
252
+ index += 1;
253
+ }
254
+ } else if (character === "*") expression += "[^/]*";
255
+ else if (character === "?") expression += "[^/]";
256
+ else expression += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
257
+ }
258
+ return new RegExp(`${expression}$`, "u");
259
+ }
260
+
261
+ function scopeGlobDiagnostics(scopes, trackedFiles) {
262
+ const diagnostics = [];
263
+ for (const entry of scopes) {
264
+ for (const binding of entry.bindings) {
265
+ if (binding.kind !== "path_glob") continue;
266
+ const matches = globRegularExpression(binding.value);
267
+ if (trackedFiles.some((file) => matches.test(file))) continue;
268
+ diagnostics.push({
269
+ ...baseDiagnostic(
270
+ "AUTH_ANCHOR_SCOPE_GLOB_UNRESOLVED",
271
+ `scope path_glob matches no Git tracked file: scope=${entry.scope}; glob=${binding.value}`,
272
+ "correct the explicit scope path_glob or add the intended file to the Git index; do not infer a repository binding",
273
+ binding.location.file,
274
+ binding.location.range,
275
+ binding.location.sourcePointer,
276
+ ),
277
+ scope: entry.scope,
278
+ glob: binding.value,
279
+ });
280
+ }
281
+ }
282
+ return diagnostics;
283
+ }
284
+
285
+ async function repositoryScriptNames(repository) {
286
+ const manifest = path.join(repository, "package.json");
287
+ let bytes;
288
+ try {
289
+ bytes = await readFile(manifest);
290
+ } catch (error) {
291
+ return { names: new Set(), error: `repository root package.json is not readable: ${error.message}` };
292
+ }
293
+ let text;
294
+ try {
295
+ text = UTF8.decode(bytes);
296
+ } catch {
297
+ return { names: new Set(), error: "repository root package.json is not valid UTF-8" };
298
+ }
299
+ let value;
300
+ try {
301
+ value = JSON.parse(text);
302
+ } catch (error) {
303
+ return { names: new Set(), error: `repository root package.json is not valid JSON: ${error.message}` };
304
+ }
305
+ if (value === null || typeof value !== "object" || Array.isArray(value)
306
+ || value.scripts === null || typeof value.scripts !== "object" || Array.isArray(value.scripts)) {
307
+ return { names: new Set(), error: "repository root package.json requires one scripts object" };
308
+ }
309
+ return { names: new Set(Object.keys(value.scripts)), error: null };
310
+ }
311
+
312
+ function compareAnchorDiagnostic(left, right) {
313
+ const leftAnchor = left.unitId !== undefined;
314
+ const rightAnchor = right.unitId !== undefined;
315
+ if (leftAnchor && rightAnchor) {
316
+ return compareText(left.unitId, right.unitId)
317
+ || compareText(left.anchor, right.anchor)
318
+ || compareText(left.field, right.field)
319
+ || compareText(left.code, right.code);
320
+ }
321
+ if (leftAnchor !== rightAnchor) return leftAnchor ? -1 : 1;
322
+ if (left.scope !== undefined && right.scope !== undefined) {
323
+ return compareText(left.scope, right.scope) || compareText(left.glob, right.glob);
324
+ }
325
+ return compareText(left.path, right.path)
326
+ || left.range.start.line - right.range.start.line
327
+ || left.range.start.column - right.range.start.column
328
+ || compareText(left.code, right.code);
329
+ }
330
+
331
+ function canonicalAnchorsBytes(summary, diagnostics) {
332
+ return Buffer.byteLength(JSON.stringify({
333
+ format: ANCHORS_FORMAT,
334
+ complete: true,
335
+ summary,
336
+ diagnostics,
337
+ }), "utf8");
338
+ }
339
+
340
+ function corpusInputDiagnostic(error) {
341
+ return baseDiagnostic(
342
+ "AUTH_ANCHOR_CORPUS_INVALID",
343
+ error.message,
344
+ "provide one complete admitted canonical authority corpus; partial or unsupported input is forbidden",
345
+ );
346
+ }
347
+
348
+ function repositoryDiagnostic(error) {
349
+ return baseDiagnostic(
350
+ "AUTH_ANCHOR_REPOSITORY_INVALID",
351
+ error.message,
352
+ "provide one exact readable Git worktree root; lexical anchors are never checked against a partial repository inventory",
353
+ error.file ?? ".",
354
+ );
355
+ }
356
+
357
+ export async function anchorsAuthorityRepository(repositoryPath, specPath, scopeBindingsPath, { maxUnits, maxAnchors, maxBytes, embeddedPaths = false }) {
358
+ if (![maxUnits, maxAnchors, maxBytes].every(validBudget)) {
359
+ return refused([baseDiagnostic(
360
+ "AUTH_ANCHOR_BUDGET",
361
+ "authority anchor budgets must be positive safe integers",
362
+ "provide explicit positive safe max-units, max-anchors, and max-bytes budgets",
363
+ )]);
364
+ }
365
+
366
+ try {
367
+ await resolveRepositoryCorpus(repositoryPath, specPath, "authority anchor");
368
+ } catch (error) {
369
+ return refused([baseDiagnostic("AUTH_ANCHOR_CORPUS_INVALID", error.message, "provide the repository authority corpus at .nimi/spec")]);
370
+ }
371
+
372
+ let compiled;
373
+ try {
374
+ compiled = await compileAuthorityPath(specPath);
375
+ } catch (error) {
376
+ if (error instanceof AuthorityInputError) return refused([corpusInputDiagnostic(error)]);
377
+ throw error;
378
+ }
379
+ if (!compiled.ok) return refused(compiled.diagnostics, { fileCount: compiled.fileCount });
380
+
381
+ const extracted = extractCorpusAnchors(compiled);
382
+ if (extracted.active.length > maxUnits) {
383
+ return refused([budgetDiagnostic(
384
+ "active units",
385
+ extracted.active.length,
386
+ maxUnits,
387
+ unitLocation(compiled, extracted.active[0]?.unitIndex ?? 0),
388
+ )], { fileCount: compiled.fileCount });
389
+ }
390
+
391
+ let scopes = [];
392
+ if (scopeBindingsPath !== null) {
393
+ const parsed = await loadScopeBindings(scopeBindingsPath);
394
+ if (!parsed.ok) return refused(parsed.diagnostics, { fileCount: compiled.fileCount });
395
+ scopes = parsed.scopes;
396
+ }
397
+
398
+ let inventory;
399
+ if (embeddedPaths) {
400
+ try {
401
+ inventory = await repositoryInventory(repositoryPath);
402
+ } catch (error) {
403
+ if (error instanceof AuthorityReviewRefusal) return refused([repositoryDiagnostic(error)], { fileCount: compiled.fileCount });
404
+ throw error;
405
+ }
406
+ extracted.anchors.push(...extractCorpusEmbeddedPathAnchors(compiled, inventory.files));
407
+ extracted.anchors.sort(compareAnchor);
408
+ }
409
+
410
+ if (extracted.anchors.length > maxAnchors) {
411
+ return refused([budgetDiagnostic(
412
+ "lexical anchors",
413
+ extracted.anchors.length,
414
+ maxAnchors,
415
+ extracted.anchors[0]?.location,
416
+ )], { fileCount: compiled.fileCount });
417
+ }
418
+
419
+ if (inventory === undefined) {
420
+ try {
421
+ inventory = await repositoryInventory(repositoryPath);
422
+ } catch (error) {
423
+ if (error instanceof AuthorityReviewRefusal) return refused([repositoryDiagnostic(error)], { fileCount: compiled.fileCount });
424
+ throw error;
425
+ }
426
+ }
427
+ const tracked = new Set(inventory.files);
428
+ const diagnostics = scopeGlobDiagnostics(scopes, inventory.files);
429
+ const scriptAnchors = extracted.anchors.filter((anchor) => anchor.type === "script");
430
+ const scripts = scriptAnchors.length > 0
431
+ ? await repositoryScriptNames(inventory.repository)
432
+ : { names: new Set(), error: null };
433
+
434
+ for (const anchor of extracted.anchors) {
435
+ if (anchor.type === "path" && !tracked.has(anchor.text)) {
436
+ diagnostics.push(anchorDiagnostic(
437
+ anchor,
438
+ "AUTH_ANCHOR_PATH_UNRESOLVED",
439
+ `active unit ${anchor.unitId} field ${anchor.field} references a path anchor that is not a Git tracked file: ${anchor.text}`,
440
+ "correct the exact lexical path or add the intended file to the Git index; path normalization and inference are forbidden",
441
+ ));
442
+ } else if (anchor.type === "embedded-path" && !tracked.has(anchor.text)) {
443
+ diagnostics.push(anchorDiagnostic(
444
+ anchor,
445
+ "AUTH_ANCHOR_EMBEDDED_PATH_UNRESOLVED",
446
+ `active unit ${anchor.unitId} field ${anchor.field} references an embedded repository path that is not a Git tracked file: ${anchor.text}`,
447
+ "correct the embedded repository path in the unit text or add the intended file to the Git index; do not infer a target",
448
+ ));
449
+ } else if (anchor.type === "script" && !scripts.names.has(anchor.script)) {
450
+ const detail = scripts.error === null ? "is absent from repository root package.json scripts" : `cannot be resolved because ${scripts.error}`;
451
+ diagnostics.push(anchorDiagnostic(
452
+ anchor,
453
+ "AUTH_ANCHOR_SCRIPT_UNRESOLVED",
454
+ `active unit ${anchor.unitId} field ${anchor.field} references script ${anchor.text}, which ${detail}`,
455
+ "declare the exact repository-root package script or correct the lexical anchor; no command is executed",
456
+ ));
457
+ }
458
+ }
459
+ diagnostics.sort(compareAnchorDiagnostic);
460
+ const summary = {
461
+ units: extracted.active.length,
462
+ anchorsChecked: extracted.anchors.length,
463
+ diagnostics: diagnostics.length,
464
+ };
465
+ const anchorsBytes = canonicalAnchorsBytes(summary, diagnostics);
466
+ if (anchorsBytes > maxBytes) {
467
+ return refused([budgetDiagnostic("UTF-8 bytes", anchorsBytes, maxBytes)], {
468
+ fileCount: compiled.fileCount,
469
+ anchorsBytes,
470
+ });
471
+ }
472
+ return {
473
+ ok: diagnostics.length === 0,
474
+ complete: true,
475
+ partial: false,
476
+ diagnostics,
477
+ fileCount: compiled.fileCount,
478
+ unitCount: extracted.active.length,
479
+ anchorsChecked: extracted.anchors.length,
480
+ anchorsBytes,
481
+ trackedPathSetDigest: inventory.trackedPathSetDigest,
482
+ };
483
+ }
484
+
485
+ export function refuseAnchorsOutputBudget(requiredBytes, admittedBytes) {
486
+ return refused([budgetDiagnostic("UTF-8 bytes", requiredBytes, admittedBytes)], {
487
+ anchorsBytes: requiredBytes,
488
+ });
489
+ }
490
+
491
+ export { ANCHORS_FORMAT, CODE_EXTENSIONS };