@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
@@ -0,0 +1,105 @@
1
+ import { compareText, makeDiagnostic, REPAIRS, sortDiagnostics, sourcePointer as absoluteSourcePointer } from "./diagnostics.mjs";
2
+
3
+ function addMappedLeaf(source, fields, diagnostics, outputPointer, sourcePointer) {
4
+ const range = source.locations.get(sourcePointer);
5
+ if (!range) {
6
+ diagnostics.push(makeDiagnostic({
7
+ code: "AUTH_SOURCE_MAP_MISSING",
8
+ file: source.file,
9
+ range: source.locations.get("") ?? source.locator.range(0),
10
+ pointer: absoluteSourcePointer(source, sourcePointer),
11
+ reason: `AuthorityIR leaf ${outputPointer} has no source range at ${sourcePointer}`,
12
+ repair: REPAIRS.structural,
13
+ }));
14
+ } else {
15
+ fields[outputPointer] = { file: source.file, range, sourcePointer: absoluteSourcePointer(source, sourcePointer) };
16
+ }
17
+ }
18
+
19
+ function collectIrLeaves(value, pointer = "", leaves = new Set()) {
20
+ if (Array.isArray(value)) {
21
+ if (value.length === 0) leaves.add(pointer);
22
+ else value.forEach((entry, index) => collectIrLeaves(entry, `${pointer}/${index}`, leaves));
23
+ return leaves;
24
+ }
25
+ if (value !== null && typeof value === "object") {
26
+ const entries = Object.entries(value);
27
+ if (entries.length === 0) leaves.add(pointer);
28
+ else entries.forEach(([key, entry]) => collectIrLeaves(entry, `${pointer}/${key}`, leaves));
29
+ return leaves;
30
+ }
31
+ leaves.add(pointer);
32
+ return leaves;
33
+ }
34
+
35
+ function mapUnit(source, outputIndex, fields, diagnostics) {
36
+ const base = `/units/${outputIndex}`;
37
+ for (const field of ["id", "kind", "owner", "lifecycle"]) {
38
+ addMappedLeaf(source, fields, diagnostics, `${base}/${field}`, `/${field}`);
39
+ }
40
+ addMappedLeaf(source, fields, diagnostics, `${base}/metadata/title`, "/title");
41
+ if (source.data.reason !== undefined) addMappedLeaf(source, fields, diagnostics, `${base}/metadata/reason`, "/reason");
42
+ if (source.data.lifecycle === "active" && source.data.kind === "definition") {
43
+ addMappedLeaf(source, fields, diagnostics, `${base}/semantic/meaning`, "/meaning");
44
+ }
45
+ if (source.data.lifecycle === "active" && source.data.kind === "rule") {
46
+ for (const field of ["modality", "statement", "condition", "failure"]) {
47
+ addMappedLeaf(source, fields, diagnostics, `${base}/semantic/${field}`, `/${field}`);
48
+ }
49
+ source.data.scope
50
+ .map((value, sourceIndex) => ({ value, sourceIndex }))
51
+ .sort((left, right) => compareText(left.value, right.value))
52
+ .forEach((entry, scopeIndex) => addMappedLeaf(source, fields, diagnostics, `${base}/semantic/scope/${scopeIndex}`, `/scope/${entry.sourceIndex}`));
53
+ }
54
+ if (source.data.relations.length === 0) {
55
+ addMappedLeaf(source, fields, diagnostics, `${base}/relations`, "/relations");
56
+ } else {
57
+ source.data.relations
58
+ .map((relation, sourceIndex) => ({ relation, sourceIndex }))
59
+ .sort((left, right) => compareText(left.relation.type, right.relation.type) || compareText(left.relation.target, right.relation.target))
60
+ .forEach((entry, relationIndex) => {
61
+ const outputPointer = `${base}/relations/${relationIndex}`;
62
+ const sourcePointer = `/relations/${entry.sourceIndex}`;
63
+ addMappedLeaf(source, fields, diagnostics, `${outputPointer}/type`, `${sourcePointer}/type`);
64
+ addMappedLeaf(source, fields, diagnostics, `${outputPointer}/target`, `${sourcePointer}/target`);
65
+ });
66
+ }
67
+ }
68
+
69
+ export function buildSourceMap(sources, ir) {
70
+ const diagnostics = [];
71
+ const fields = {};
72
+ const orderedSources = [...sources].sort((left, right) => compareText(left.data.id, right.data.id));
73
+ orderedSources.forEach((source, outputIndex) => mapUnit(source, outputIndex, fields, diagnostics));
74
+ const expectedLeaves = collectIrLeaves(ir);
75
+ for (const pointer of Object.keys(fields)) {
76
+ if (!expectedLeaves.has(pointer)) {
77
+ const mapped = fields[pointer];
78
+ diagnostics.push(makeDiagnostic({
79
+ code: "AUTH_SOURCE_MAP_MISSING",
80
+ file: mapped.file,
81
+ range: mapped.range,
82
+ pointer: mapped.sourcePointer,
83
+ reason: `SourceMap entry does not address an AuthorityIR leaf: ${pointer}`,
84
+ repair: REPAIRS.structural,
85
+ }));
86
+ }
87
+ }
88
+ for (const pointer of expectedLeaves) {
89
+ if (!Object.hasOwn(fields, pointer)) {
90
+ const match = pointer.match(/^\/units\/(\d+)/);
91
+ const source = orderedSources[Number(match?.[1] ?? 0)] ?? orderedSources[0];
92
+ diagnostics.push(makeDiagnostic({
93
+ code: "AUTH_SOURCE_MAP_MISSING",
94
+ file: source.file,
95
+ range: source.locations.get("") ?? source.locator.range(0),
96
+ pointer: absoluteSourcePointer(source, ""),
97
+ reason: `AuthorityIR leaf is not mapped: ${pointer}`,
98
+ repair: REPAIRS.structural,
99
+ }));
100
+ }
101
+ }
102
+ return { ok: diagnostics.length === 0, diagnostics: sortDiagnostics(diagnostics), sourceMap: { fields } };
103
+ }
104
+
105
+ export { collectIrLeaves };
@@ -0,0 +1,167 @@
1
+ import { makeDiagnostic, portablePath, REPAIRS, createLocator } from "./diagnostics.mjs";
2
+ import { FRONTMATTER_KEY_ORDER, parseRestrictedYaml, stringifyCanonicalYaml } from "./source-yaml.mjs";
3
+
4
+ const SECTION_FIELDS = new Map([
5
+ ["Meaning", "meaning"],
6
+ ["Statement", "statement"],
7
+ ["Condition", "condition"],
8
+ ["Failure", "failure"],
9
+ ["Removal reason", "reason"],
10
+ ]);
11
+ const BODY_ORDER = [
12
+ ["meaning", "Meaning"],
13
+ ["statement", "Statement"],
14
+ ["condition", "Condition"],
15
+ ["failure", "Failure"],
16
+ ["reason", "Removal reason"],
17
+ ];
18
+
19
+ function lineTable(text) {
20
+ const lines = [];
21
+ let start = 0;
22
+ for (let index = 0; index <= text.length; index += 1) {
23
+ if (index === text.length || text[index] === "\n") {
24
+ const raw = text.slice(start, index);
25
+ lines.push({ text: raw.endsWith("\r") ? raw.slice(0, -1) : raw, start, end: index });
26
+ start = index + 1;
27
+ }
28
+ }
29
+ return lines;
30
+ }
31
+
32
+ function ambiguous(file, locator, line, reason) {
33
+ return makeDiagnostic({
34
+ code: "AUTH_MARKDOWN_AMBIGUOUS",
35
+ file,
36
+ range: locator.range(line.start, line.end),
37
+ reason,
38
+ repair: REPAIRS.structural,
39
+ });
40
+ }
41
+
42
+ function isPlainMarkdownSlot(value) {
43
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value || /[\r\n\t]/.test(value)) return false;
44
+ if (!/^[\p{L}\p{N}\p{M}\p{Zs}\p{P}\p{S}]+$/u.test(value)) return false;
45
+ if (/[`*_\[\]<>\\~]/u.test(value)) return false;
46
+ if (/^(?:#{1,6}(?:\s|$)|>|[-+*](?:\s|$)|\d+[.)](?:\s|$)|(?:-{3,}|={3,})(?:\s|$))/u.test(value)) return false;
47
+ return true;
48
+ }
49
+
50
+ export function parseMarkdownAuthority(text, absolutePath, options = {}) {
51
+ const file = portablePath(absolutePath, options.cwd);
52
+ const locator = createLocator(text);
53
+ const lines = lineTable(text);
54
+ const diagnostics = [];
55
+ if (lines[0]?.text !== "---") {
56
+ diagnostics.push(ambiguous(file, locator, lines[0] ?? { start: 0, end: 0 }, "Markdown authority must start with YAML front matter"));
57
+ return { ok: false, file, locator, diagnostics, locations: new Map(), data: null, profile: "markdown" };
58
+ }
59
+ const closingIndex = lines.findIndex((line, index) => index > 0 && line.text === "---");
60
+ if (closingIndex < 0) {
61
+ diagnostics.push(ambiguous(file, locator, lines[0], "Markdown front matter is missing its closing delimiter"));
62
+ return { ok: false, file, locator, diagnostics, locations: new Map(), data: null, profile: "markdown" };
63
+ }
64
+ const frontStart = lines[0].end + 1;
65
+ const frontEnd = lines[closingIndex].start;
66
+ const front = text.slice(frontStart, frontEnd);
67
+ const parsedFront = parseRestrictedYaml(front, absolutePath, { ...options, offset: frontStart, fullText: text, keyOrder: FRONTMATTER_KEY_ORDER });
68
+ if (!parsedFront.ok) return { ...parsedFront, profile: "markdown" };
69
+ const data = { ...parsedFront.data };
70
+ const frontKeys = Object.keys(parsedFront.data);
71
+ const locations = new Map(parsedFront.locations);
72
+ const keyLocations = new Map(parsedFront.keyLocations);
73
+ const insertionPoints = new Map(parsedFront.insertionPoints);
74
+ const closingInsertion = locator.range(lines[closingIndex].start, lines[closingIndex].start);
75
+ if (!Object.hasOwn(data, "relations")) insertionPoints.set("/relations", closingInsertion);
76
+ locations.set("", locator.range(0, text.length));
77
+ let index = closingIndex + 1;
78
+ while (index < lines.length && lines[index].text === "") index += 1;
79
+ if (lines[index]?.text.startsWith("# ")) {
80
+ const title = lines[index].text.slice(2);
81
+ data.title = title;
82
+ locations.set("/title", locator.range(lines[index].start + 2, lines[index].end));
83
+ if (!isPlainMarkdownSlot(title)) diagnostics.push(ambiguous(file, locator, lines[index], "Markdown title must be one plain non-empty text line"));
84
+ index += 1;
85
+ } else if (/^#(?!#)/.test(lines[index]?.text ?? "")) {
86
+ diagnostics.push(ambiguous(file, locator, lines[index], "Markdown authority allows exactly one level-one title"));
87
+ index += 1;
88
+ }
89
+ const seenSections = new Set();
90
+ const sectionHeadings = new Map();
91
+ while (index < lines.length) {
92
+ while (index < lines.length && lines[index].text === "") index += 1;
93
+ if (index >= lines.length || (index === lines.length - 1 && lines[index].text === "")) break;
94
+ const heading = lines[index];
95
+ if (!heading.text.startsWith("## ")) {
96
+ diagnostics.push(ambiguous(file, locator, heading, "prose and constructs outside known Markdown authority slots are not admitted"));
97
+ index += 1;
98
+ continue;
99
+ }
100
+ const headingName = heading.text.slice(3);
101
+ const field = SECTION_FIELDS.get(headingName);
102
+ if (!field) {
103
+ diagnostics.push(ambiguous(file, locator, heading, `unknown Markdown authority section: ${headingName}`));
104
+ index += 1;
105
+ continue;
106
+ }
107
+ if (seenSections.has(field)) {
108
+ diagnostics.push(ambiguous(file, locator, heading, `duplicate Markdown authority section: ${headingName}`));
109
+ index += 1;
110
+ continue;
111
+ }
112
+ seenSections.add(field);
113
+ sectionHeadings.set(field, locator.range(heading.start, heading.end));
114
+ index += 1;
115
+ while (index < lines.length && lines[index].text === "") index += 1;
116
+ const body = lines[index];
117
+ if (!body || body.text.startsWith("#")) {
118
+ data[field] = "";
119
+ locations.set(`/${field}`, locator.range(heading.end, heading.end));
120
+ continue;
121
+ }
122
+ if (!isPlainMarkdownSlot(body.text)) {
123
+ diagnostics.push(ambiguous(file, locator, body, `${headingName} must contain exactly one plain text line`));
124
+ }
125
+ data[field] = body.text;
126
+ locations.set(`/${field}`, locator.range(body.start, body.end));
127
+ index += 1;
128
+ if (index < lines.length && lines[index].text !== "" && !lines[index].text.startsWith("## ")) {
129
+ diagnostics.push(ambiguous(file, locator, lines[index], `${headingName} must contain exactly one text line`));
130
+ index += 1;
131
+ }
132
+ }
133
+ const firstBodyPosition = locator.range(lines[closingIndex].end + 1, lines[closingIndex].end + 1);
134
+ if (!Object.hasOwn(data, "title")) insertionPoints.set("/title", firstBodyPosition);
135
+ for (let bodyIndex = 0; bodyIndex < BODY_ORDER.length; bodyIndex += 1) {
136
+ const [field] = BODY_ORDER[bodyIndex];
137
+ if (Object.hasOwn(data, field)) continue;
138
+ const nextField = BODY_ORDER.slice(bodyIndex + 1).map(([candidate]) => candidate).find((candidate) => sectionHeadings.has(candidate));
139
+ insertionPoints.set(`/${field}`, nextField ? { start: sectionHeadings.get(nextField).start, end: sectionHeadings.get(nextField).start } : locator.range(text.length, text.length));
140
+ }
141
+ return {
142
+ ok: diagnostics.length === 0,
143
+ file,
144
+ locator,
145
+ diagnostics,
146
+ locations,
147
+ keyLocations,
148
+ insertionPoints,
149
+ data,
150
+ profile: "markdown",
151
+ sourceText: text,
152
+ frontKeys,
153
+ };
154
+ }
155
+
156
+ export function stringifyCanonicalMarkdown(data) {
157
+ const front = {};
158
+ for (const key of ["format", "id", "kind", "owner", "lifecycle", "modality", "scope", "relations"]) {
159
+ if (Object.hasOwn(data, key)) front[key] = data[key];
160
+ }
161
+ let output = `---\n${stringifyCanonicalYaml(front, { frontmatter: true }).trimEnd()}\n---`;
162
+ if (Object.hasOwn(data, "title")) output += `\n# ${data.title}`;
163
+ for (const [field, heading] of BODY_ORDER) {
164
+ if (Object.hasOwn(data, field)) output += `\n\n## ${heading}\n\n${String(data[field])}`;
165
+ }
166
+ return `${output}\n`;
167
+ }
@@ -0,0 +1,308 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import YAML, { isAlias, isMap, isScalar, isSeq, visit } from "yaml";
5
+
6
+ import { compareText, createLocator, makeDiagnostic, portablePath, REPAIRS } from "./diagnostics.mjs";
7
+
8
+ const CONTRACT_PATH = fileURLToPath(new URL("../../../contracts/authority-source.schema.yaml", import.meta.url));
9
+ let contractPromise = null;
10
+
11
+ export async function loadAuthorityContract() {
12
+ if (!contractPromise) {
13
+ contractPromise = readFile(CONTRACT_PATH, "utf8").then((text) => {
14
+ const value = YAML.parse(text);
15
+ if (value?.version !== 1 || value?.contract?.id !== "nimicoding.authority-source.v2") {
16
+ throw new Error("installed authority source contract is invalid");
17
+ }
18
+ return value;
19
+ });
20
+ }
21
+ return contractPromise;
22
+ }
23
+
24
+ function pointerToken(value) {
25
+ return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
26
+ }
27
+
28
+ function nodeRange(node, locator, offset) {
29
+ const range = node?.range;
30
+ if (!Array.isArray(range)) return locator.range(offset, offset);
31
+ return locator.range(offset + range[0], offset + (range[1] ?? range[0]));
32
+ }
33
+
34
+ function collectLocations(node, locator, offset, pointer, locations, keyLocations) {
35
+ if (!node) return;
36
+ locations.set(pointer, nodeRange(node, locator, offset));
37
+ if (isMap(node)) {
38
+ for (const pair of node.items) {
39
+ const key = isScalar(pair.key) ? String(pair.key.value) : "";
40
+ const childPointer = `${pointer}/${pointerToken(key)}`;
41
+ keyLocations.set(childPointer, nodeRange(pair.key, locator, offset));
42
+ locations.set(childPointer, nodeRange(pair.value ?? pair.key, locator, offset));
43
+ collectLocations(pair.value, locator, offset, childPointer, locations, keyLocations);
44
+ }
45
+ } else if (isSeq(node)) {
46
+ node.items.forEach((item, index) => collectLocations(item, locator, offset, `${pointer}/${index}`, locations, keyLocations));
47
+ }
48
+ }
49
+
50
+ function zeroAt(position) {
51
+ return { start: position, end: position };
52
+ }
53
+
54
+ export function buildInsertionPoints(data, locations, keyLocations, keyOrder) {
55
+ const insertionPoints = new Map();
56
+ const rootEnd = locations.get("")?.end ?? { line: 1, column: 1 };
57
+ for (let index = 0; index < keyOrder.length; index += 1) {
58
+ const field = keyOrder[index];
59
+ if (Object.hasOwn(data, field)) continue;
60
+ const nextField = keyOrder.slice(index + 1).find((candidate) => keyLocations.has(`/${candidate}`));
61
+ insertionPoints.set(`/${field}`, nextField ? zeroAt(keyLocations.get(`/${nextField}`).start) : zeroAt(rootEnd));
62
+ }
63
+ if (Array.isArray(data.relations)) {
64
+ data.relations.forEach((relation, index) => {
65
+ if (!relation || typeof relation !== "object" || Array.isArray(relation)) return;
66
+ const pointer = `/relations/${index}`;
67
+ const relationEnd = locations.get(pointer)?.end ?? rootEnd;
68
+ if (!Object.hasOwn(relation, "type")) {
69
+ insertionPoints.set(`${pointer}/type`, keyLocations.has(`${pointer}/target`)
70
+ ? zeroAt(keyLocations.get(`${pointer}/target`).start)
71
+ : zeroAt(relationEnd));
72
+ }
73
+ if (!Object.hasOwn(relation, "target")) insertionPoints.set(`${pointer}/target`, zeroAt(relationEnd));
74
+ });
75
+ }
76
+ return insertionPoints;
77
+ }
78
+
79
+ function yamlErrorDiagnostic(error, file, locator, offset) {
80
+ const positions = Array.isArray(error.pos) ? error.pos : [0, 0];
81
+ const duplicate = /keys must be unique|duplicate key/i.test(error.message);
82
+ return makeDiagnostic({
83
+ code: duplicate ? "AUTH_DUPLICATE_KEY" : "AUTH_SYNTAX_UNSUPPORTED",
84
+ file,
85
+ range: locator.range(offset + positions[0], offset + (positions[1] ?? positions[0])),
86
+ reason: duplicate ? "YAML mapping keys must be unique" : `unsupported YAML syntax: ${error.message.split(" at line")[0]}`,
87
+ repair: duplicate ? REPAIRS.duplicate : REPAIRS.structural,
88
+ });
89
+ }
90
+
91
+ export function parseRestrictedYaml(text, absolutePath, options = {}) {
92
+ const offset = options.offset ?? 0;
93
+ const fullText = options.fullText ?? text;
94
+ const file = portablePath(absolutePath, options.cwd);
95
+ const locator = createLocator(fullText);
96
+ const diagnostics = [];
97
+ const documents = YAML.parseAllDocuments(text, {
98
+ keepSourceTokens: true,
99
+ prettyErrors: false,
100
+ strict: true,
101
+ uniqueKeys: true,
102
+ version: "1.2",
103
+ });
104
+ if (documents.length !== 1) {
105
+ diagnostics.push(makeDiagnostic({
106
+ code: "AUTH_SYNTAX_UNSUPPORTED",
107
+ file,
108
+ range: locator.range(offset, offset + text.length),
109
+ reason: "authority source must contain exactly one YAML document",
110
+ repair: REPAIRS.structural,
111
+ }));
112
+ }
113
+ const document = documents[0];
114
+ for (const error of document?.errors ?? []) diagnostics.push(yamlErrorDiagnostic(error, file, locator, offset));
115
+ if (diagnostics.length > 0 || !document) {
116
+ return { ok: false, file, locator, diagnostics, locations: new Map(), keyLocations: new Map(), insertionPoints: new Map(), data: null };
117
+ }
118
+ if (!isMap(document.contents)) {
119
+ diagnostics.push(makeDiagnostic({
120
+ code: "AUTH_SYNTAX_UNSUPPORTED",
121
+ file,
122
+ range: nodeRange(document.contents, locator, offset),
123
+ reason: "authority YAML must be one top-level mapping",
124
+ repair: REPAIRS.structural,
125
+ }));
126
+ }
127
+ if (/^%/.test(text)) {
128
+ diagnostics.push(makeDiagnostic({
129
+ code: "AUTH_SYNTAX_UNSUPPORTED",
130
+ file,
131
+ range: locator.range(offset, offset + Math.min(text.length, text.indexOf("\n") + 1 || text.length)),
132
+ reason: "YAML directives are not admitted",
133
+ repair: REPAIRS.structural,
134
+ }));
135
+ }
136
+ if (document.comment || document.commentBefore) {
137
+ const searchStart = document.contents?.range?.[2] ?? document.contents?.range?.[1] ?? 0;
138
+ const commentStart = text.indexOf("#", searchStart);
139
+ diagnostics.push(makeDiagnostic({
140
+ code: "AUTH_SYNTAX_UNSUPPORTED",
141
+ file,
142
+ range: locator.range(offset + Math.max(0, commentStart), offset + (commentStart < 0 ? text.length : commentStart + 1)),
143
+ reason: "YAML comments are not admitted in canonical authority",
144
+ repair: REPAIRS.structural,
145
+ }));
146
+ }
147
+ if (document.contents) {
148
+ visit(document, (_key, node) => {
149
+ if (!node || typeof node !== "object") return;
150
+ let reason = null;
151
+ if (node.comment || node.commentBefore) reason = "YAML comments are not admitted in canonical authority";
152
+ else if (isAlias(node)) reason = "YAML aliases are not admitted";
153
+ else if (node.anchor) reason = "YAML anchors are not admitted";
154
+ else if (node.tag) reason = "explicit YAML tags are not admitted";
155
+ if (reason) diagnostics.push(makeDiagnostic({
156
+ code: "AUTH_SYNTAX_UNSUPPORTED",
157
+ file,
158
+ range: nodeRange(node, locator, offset),
159
+ reason,
160
+ repair: REPAIRS.structural,
161
+ }));
162
+ if (node.constructor?.name === "Pair" && !isScalar(node.key)) {
163
+ diagnostics.push(makeDiagnostic({
164
+ code: "AUTH_SYNTAX_UNSUPPORTED",
165
+ file,
166
+ range: nodeRange(node.key, locator, offset),
167
+ reason: "complex YAML mapping keys are not admitted",
168
+ repair: REPAIRS.structural,
169
+ }));
170
+ }
171
+ });
172
+ }
173
+ if (diagnostics.length > 0) return { ok: false, file, locator, diagnostics, locations: new Map(), keyLocations: new Map(), insertionPoints: new Map(), data: null };
174
+ const locations = new Map();
175
+ const keyLocations = new Map();
176
+ collectLocations(document.contents, locator, offset, "", locations, keyLocations);
177
+ const data = document.toJS({ mapAsMap: false });
178
+ const insertionPoints = buildInsertionPoints(data, locations, keyLocations, options.keyOrder ?? []);
179
+ return {
180
+ ok: true,
181
+ file,
182
+ locator,
183
+ diagnostics,
184
+ locations,
185
+ keyLocations,
186
+ insertionPoints,
187
+ data,
188
+ yamlDocument: document,
189
+ sourceText: fullText,
190
+ };
191
+ }
192
+
193
+ export const AUTHORITY_FORMAT_V2 = "nimicoding.authority/v2";
194
+ export const YAML_CONTAINER_KEY_ORDER = ["format", "owner", "scope", "units"];
195
+ export const YAML_UNIT_KEY_ORDER = [
196
+ "id", "kind", "owner", "lifecycle", "title", "meaning", "modality", "scope",
197
+ "statement", "condition", "failure", "reason", "relations",
198
+ ];
199
+ export const FRONTMATTER_KEY_ORDER = ["format", "id", "kind", "owner", "lifecycle", "modality", "scope", "relations"];
200
+
201
+ function orderedObject(data, order) {
202
+ return Object.fromEntries(order.filter((key) => Object.hasOwn(data, key)).map((key) => [key, data[key]]));
203
+ }
204
+
205
+ function normalizedUnit(data, order = YAML_UNIT_KEY_ORDER, defaults = null) {
206
+ const next = orderedObject(data, order);
207
+ if (defaults) {
208
+ if (next.owner === defaults.owner) delete next.owner;
209
+ if (next.lifecycle === "active") delete next.lifecycle;
210
+ if (Array.isArray(next.relations) && next.relations.length === 0) delete next.relations;
211
+ if (next.kind === "rule" && next.lifecycle !== "removed" && Array.isArray(next.scope)
212
+ && next.scope.length === 1 && next.scope[0] === defaults.scope) delete next.scope;
213
+ }
214
+ if (Array.isArray(next.scope)) next.scope = [...next.scope].sort(compareText);
215
+ if (Array.isArray(next.relations)) {
216
+ next.relations = next.relations.map((entry) => ({ type: entry.type, target: entry.target })).sort((left, right) => (
217
+ compareText(left.type, right.type) || compareText(left.target, right.target)
218
+ ));
219
+ }
220
+ return next;
221
+ }
222
+
223
+ export function normalizedData(data, { frontmatter = false, container = false } = {}) {
224
+ if (frontmatter === container) throw new TypeError("canonical YAML serialization requires exactly one explicit profile: container or frontmatter");
225
+ if (container) {
226
+ const v2 = data?.format === AUTHORITY_FORMAT_V2;
227
+ const defaults = v2 ? { owner: data.owner, scope: data.scope } : null;
228
+ return {
229
+ ...(Object.hasOwn(data, "format") ? { format: data.format } : {}),
230
+ ...(v2 && Object.hasOwn(data, "owner") ? { owner: data.owner } : {}),
231
+ ...(v2 && Object.hasOwn(data, "scope") ? { scope: data.scope } : {}),
232
+ ...(Object.hasOwn(data, "units") ? { units: Array.isArray(data.units) ? data.units.map((unit) => normalizedUnit(unit, YAML_UNIT_KEY_ORDER, defaults)) : data.units } : {}),
233
+ };
234
+ }
235
+ return normalizedUnit(data, FRONTMATTER_KEY_ORDER);
236
+ }
237
+
238
+ export function stringifyCanonicalYaml(data, options = {}) {
239
+ return YAML.stringify(normalizedData(data, options), {
240
+ indent: 2,
241
+ lineWidth: 0,
242
+ minContentWidth: 0,
243
+ defaultStringType: "PLAIN",
244
+ defaultKeyType: "PLAIN",
245
+ });
246
+ }
247
+
248
+ function localMap(map, prefix) {
249
+ return new Map([...map.entries()]
250
+ .filter(([pointer]) => pointer === prefix || pointer.startsWith(`${prefix}/`))
251
+ .map(([pointer, range]) => [pointer.slice(prefix.length), range]));
252
+ }
253
+
254
+ function yamlUnitSource(parsed, index) {
255
+ const prefix = `/units/${index}`;
256
+ const locations = localMap(parsed.locations, prefix);
257
+ const keyLocations = localMap(parsed.keyLocations, prefix);
258
+ const authoredData = parsed.data.units[index];
259
+ let data = authoredData;
260
+ if (parsed.data.format === AUTHORITY_FORMAT_V2 && authoredData && typeof authoredData === "object" && !Array.isArray(authoredData)) {
261
+ data = { ...authoredData };
262
+ const unitRange = locations.get("") ?? parsed.locations.get(prefix);
263
+ if (!Object.hasOwn(data, "owner") && typeof parsed.data.owner === "string") {
264
+ data.owner = parsed.data.owner;
265
+ locations.set("/owner", parsed.locations.get("/owner") ?? unitRange);
266
+ }
267
+ if (!Object.hasOwn(data, "lifecycle")) {
268
+ data.lifecycle = "active";
269
+ locations.set("/lifecycle", unitRange);
270
+ }
271
+ if (!Object.hasOwn(data, "relations")) {
272
+ data.relations = [];
273
+ locations.set("/relations", unitRange);
274
+ }
275
+ if (data.kind === "rule" && data.lifecycle === "active" && !Object.hasOwn(data, "scope") && typeof parsed.data.scope === "string") {
276
+ data.scope = [parsed.data.scope];
277
+ locations.set("/scope", parsed.locations.get("/scope") ?? unitRange);
278
+ locations.set("/scope/0", parsed.locations.get("/scope") ?? unitRange);
279
+ }
280
+ }
281
+ return {
282
+ ok: true,
283
+ file: parsed.file,
284
+ locator: parsed.locator,
285
+ diagnostics: [],
286
+ locations,
287
+ keyLocations,
288
+ insertionPoints: authoredData && typeof authoredData === "object" && !Array.isArray(authoredData)
289
+ ? buildInsertionPoints(authoredData, locations, keyLocations, YAML_UNIT_KEY_ORDER)
290
+ : new Map(),
291
+ data,
292
+ authoredData,
293
+ documentFormat: parsed.data.format,
294
+ profile: "yaml",
295
+ sourcePrefix: prefix,
296
+ sourceIndex: index,
297
+ sourceText: parsed.sourceText,
298
+ };
299
+ }
300
+
301
+ export function parseYamlAuthority(text, absolutePath, options = {}) {
302
+ const parsed = parseRestrictedYaml(text, absolutePath, { ...options, keyOrder: YAML_CONTAINER_KEY_ORDER });
303
+ if (!parsed.ok) return { ...parsed, profile: "yaml", isDocument: true, units: [] };
304
+ const units = Array.isArray(parsed.data?.units)
305
+ ? parsed.data.units.map((_unit, index) => yamlUnitSource(parsed, index))
306
+ : [];
307
+ return { ...parsed, profile: "yaml", isDocument: true, units };
308
+ }
@@ -0,0 +1,97 @@
1
+ // The command surface the parser actually enforces.
2
+ //
3
+ // This is not documentation. parseOptions derives its flag admission from this
4
+ // table, so a flag that is not here is rejected at parse time, and a flag added
5
+ // here without a product contract fails the surface gate. Both directions have
6
+ // to move together or something breaks.
7
+
8
+ // anchors validates and exits non-zero on findings; the other three report and
9
+ // do not. Each contract states which it is rather than the name implying it.
10
+ export const REPOSITORY_BOUND_COMMANDS = ["anchors", "consumers", "terms", "closed-sets"];
11
+
12
+ // audit reads one complete authority input plus one explicit bindings file. It
13
+ // is corpus-bound, not repository-bound, but its public parser/help/contract
14
+ // surface is enforced by the same consistency gate.
15
+ export const CORPUS_BOUND_COMMANDS = ["audit"];
16
+ export const ENFORCED_AUTHORITY_COMMANDS = [
17
+ ...REPOSITORY_BOUND_COMMANDS,
18
+ ...CORPUS_BOUND_COMMANDS,
19
+ ];
20
+
21
+ // fmt is not repository-bound, but its flags and the formats it admits are
22
+ // exactly what the authoring contract promises, so it is compared too.
23
+ export const AUTHORING_SURFACE = {
24
+ fmt: {
25
+ contract: "nimicoding.authority-authoring.v2",
26
+ booleanFlags: ["json", "check"],
27
+ valueFlags: [],
28
+ integerFlags: [],
29
+ repeatableFlags: [],
30
+ },
31
+ };
32
+
33
+ export const AUTHORITY_SURFACE = {
34
+ audit: {
35
+ contract: "nimicoding.authority-audit.v2",
36
+ reportFormat: "nimicoding.authority-audit/v2",
37
+ corpus: "authority-input",
38
+ booleanFlags: ["json", "sarif"],
39
+ valueFlags: ["bindings"],
40
+ integerFlags: ["max-units", "max-edges", "max-bytes"],
41
+ repeatableFlags: [],
42
+ },
43
+ anchors: {
44
+ contract: "nimicoding.authority-anchors.v1",
45
+ reportFormat: "nimicoding.authority-anchors/v1",
46
+ corpus: "repository",
47
+ booleanFlags: ["json", "embedded-paths"],
48
+ valueFlags: ["spec", "scope-bindings"],
49
+ integerFlags: ["max-units", "max-anchors", "max-bytes"],
50
+ repeatableFlags: [],
51
+ },
52
+ consumers: {
53
+ contract: "nimicoding.authority-consumers.v1",
54
+ reportFormat: "nimicoding.authority-consumers/v1",
55
+ corpus: "repository",
56
+ booleanFlags: ["json"],
57
+ valueFlags: ["spec"],
58
+ integerFlags: ["max-files", "max-refs", "max-bytes"],
59
+ repeatableFlags: ["external-root"],
60
+ },
61
+ terms: {
62
+ contract: "nimicoding.authority-terms.v1",
63
+ reportFormat: "nimicoding.authority-terms/v1",
64
+ corpus: "repository",
65
+ booleanFlags: ["json", "code"],
66
+ valueFlags: ["spec", "term"],
67
+ integerFlags: ["max-units", "max-terms", "max-bytes"],
68
+ repeatableFlags: [],
69
+ },
70
+ "closed-sets": {
71
+ contract: "nimicoding.authority-closed-sets.v1",
72
+ reportFormat: "nimicoding.authority-closed-sets/v1",
73
+ corpus: "repository",
74
+ booleanFlags: ["json"],
75
+ valueFlags: ["spec"],
76
+ integerFlags: ["max-units", "max-sets", "max-bytes"],
77
+ repeatableFlags: [],
78
+ },
79
+ };
80
+
81
+ export function integerFlagAdmission() {
82
+ const admission = {};
83
+ for (const [command, entry] of Object.entries(AUTHORITY_SURFACE)) {
84
+ for (const flag of entry.integerFlags) {
85
+ const option = `--${flag}`;
86
+ admission[option] = admission[option] ?? [];
87
+ admission[option].push(command);
88
+ }
89
+ }
90
+ return admission;
91
+ }
92
+
93
+ export function booleanFlagAdmission(flag) {
94
+ return Object.entries(AUTHORITY_SURFACE)
95
+ .filter(([, entry]) => entry.booleanFlags.includes(flag))
96
+ .map(([command]) => command);
97
+ }