@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.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +297 -41
- package/README.zh-CN.md +306 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +804 -0
- package/cli/commands/clear.mjs +27 -7
- package/cli/commands/doctor.mjs +10 -1
- package/cli/commands/start.mjs +23 -27
- package/cli/commands/sync.mjs +21 -101
- package/cli/constants.mjs +7 -31
- package/cli/dev/check-authority-surface.mjs +182 -0
- package/cli/dev/check-product-conformance.mjs +889 -0
- package/cli/help.mjs +10 -17
- package/cli/index.mjs +6 -47
- package/cli/lib/authority/anchors.mjs +491 -0
- package/cli/lib/authority/audit.mjs +567 -0
- package/cli/lib/authority/change-candidates.mjs +807 -0
- package/cli/lib/authority/closed-sets.mjs +645 -0
- package/cli/lib/authority/compile.mjs +92 -0
- package/cli/lib/authority/compiled-snapshot.mjs +143 -0
- package/cli/lib/authority/consumers.mjs +520 -0
- package/cli/lib/authority/diagnostics.mjs +93 -0
- package/cli/lib/authority/diff.mjs +310 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/format.mjs +169 -0
- package/cli/lib/authority/git-snapshot.mjs +720 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +125 -0
- package/cli/lib/authority/lexicon.mjs +55 -0
- package/cli/lib/authority/query.mjs +162 -0
- package/cli/lib/authority/repository-corpus.mjs +37 -0
- package/cli/lib/authority/repository-inventory.mjs +34 -0
- package/cli/lib/authority/review.mjs +212 -0
- package/cli/lib/authority/sarif.mjs +174 -0
- package/cli/lib/authority/scope-bindings.mjs +225 -0
- package/cli/lib/authority/source-map.mjs +105 -0
- package/cli/lib/authority/source-markdown.mjs +167 -0
- package/cli/lib/authority/source-yaml.mjs +308 -0
- package/cli/lib/authority/surface.mjs +97 -0
- package/cli/lib/authority/terms.mjs +422 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +300 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +104 -105
- package/cli/lib/fs-helpers.mjs +139 -15
- package/cli/lib/internal/governance/config.mjs +0 -36
- package/cli/lib/shared.mjs +8 -21
- package/cli/lib/sync.mjs +81 -92
- package/cli/lib/value-helpers.mjs +0 -26
- package/cli/lib/yaml-helpers.mjs +1 -124
- package/cli/seeds/bootstrap.mjs +31 -87
- package/cli/seeds/seed-policy.yaml +9 -63
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +58 -0
- package/contracts/authority-verifier-bindings.schema.yaml +31 -0
- package/methodology/authority-authoring.yaml +127 -0
- package/methodology/core.yaml +34 -27
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +89 -0
- package/spec/authority-authoring.yaml +137 -0
- package/spec/authority-change-candidates.yaml +146 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +77 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +61 -0
- package/spec/authority-review.yaml +71 -0
- package/spec/authority-terms.yaml +63 -0
- package/spec/product-scope.yaml +37 -23
- package/cli/commands/blueprint-audit.mjs +0 -91
- package/cli/commands/classify-spec-tree.mjs +0 -5
- package/cli/commands/generate-spec-derived-docs.mjs +0 -151
- package/cli/commands/generate-spec-migration-plan.mjs +0 -30
- package/cli/commands/surface-validator-command.mjs +0 -49
- package/cli/commands/validate-domain-admission.mjs +0 -5
- package/cli/commands/validate-guidance-bodies.mjs +0 -5
- package/cli/commands/validate-placement.mjs +0 -5
- package/cli/commands/validate-projection-edges.mjs +0 -5
- package/cli/commands/validate-spec-audit.mjs +0 -27
- package/cli/commands/validate-spec-governance.mjs +0 -144
- package/cli/commands/validate-spec-tree.mjs +0 -27
- package/cli/commands/validate-table-family.mjs +0 -5
- package/cli/commands/validate-tracked-output-admission.mjs +0 -5
- package/cli/lib/blueprint-audit.mjs +0 -363
- package/cli/lib/contracts.mjs +0 -21
- package/cli/lib/internal/contracts-loaders.mjs +0 -47
- package/cli/lib/internal/contracts-parse.mjs +0 -192
- package/cli/lib/internal/governance/runner.mjs +0 -35
- package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
- package/cli/lib/internal/validators-shared.mjs +0 -12
- package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
- package/cli/lib/internal/validators-spec.mjs +0 -380
- package/cli/lib/validators.mjs +0 -30
- package/config/bootstrap.yaml +0 -6
- package/config/spec-generation-inputs.yaml +0 -41
- package/contracts/domain-admission.schema.yaml +0 -56
- package/contracts/migration-inventory.schema.yaml +0 -15
- package/contracts/negative-fixtures.yaml +0 -26
- package/contracts/placement-contract.schema.yaml +0 -127
- package/contracts/projection-edge.schema.yaml +0 -46
- package/contracts/shared-enums.yaml +0 -63
- package/contracts/spec-generation-audit.schema.yaml +0 -46
- package/contracts/spec-generation-inputs.schema.yaml +0 -43
- package/contracts/spec-layout.schema.yaml +0 -32
- package/contracts/surface-taxonomy.schema.yaml +0 -139
- package/contracts/table-family.schema.yaml +0 -133
- package/contracts/tracked-output-admission.schema.yaml +0 -17
- package/methodology/four-closure-policy.yaml +0 -20
- package/methodology/role-separation-policy.yaml +0 -22
- package/methodology/spec-reconstruction.yaml +0 -69
- package/spec/_meta/spec-tree-model.yaml +0 -44
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { compileAuthorityPath } from "./compile.mjs";
|
|
6
|
+
import { compareText, makeDiagnostic, portablePath } from "./diagnostics.mjs";
|
|
7
|
+
import { buildAuthorityGraphSnapshot } from "./graph.mjs";
|
|
8
|
+
|
|
9
|
+
const DISCOVERY_FORMAT = "nimicoding.authority-discovery/v2";
|
|
10
|
+
const FIELD_ORDER = ["id", "title", "owner", "scope", "meaning", "statement", "condition", "failure", "reason"];
|
|
11
|
+
const RANK_FIELDS = ["scope", "failure", "condition", "reason", "owner"];
|
|
12
|
+
const KINDS = new Set(["definition", "rule"]);
|
|
13
|
+
const LIFECYCLES = new Set(["active", "removed"]);
|
|
14
|
+
const DIRECTIONS = new Set(["incoming", "outgoing", "both"]);
|
|
15
|
+
const RELATION_TYPES = new Set(["applies_to", "supersedes"]);
|
|
16
|
+
const IDENTIFIER = /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z](?:[a-z0-9-]*[a-z0-9])?)+$/;
|
|
17
|
+
|
|
18
|
+
function orderedDiscoveryTerms(text) {
|
|
19
|
+
const normalized = String(text).normalize("NFKC");
|
|
20
|
+
const camelSplit = normalized.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2").toLowerCase();
|
|
21
|
+
return camelSplit.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeDiscoveryTerms(text) {
|
|
25
|
+
return [...new Set(orderedDiscoveryTerms(text))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fieldValues(unit) {
|
|
29
|
+
return {
|
|
30
|
+
id: [unit.id],
|
|
31
|
+
title: [unit.metadata.title],
|
|
32
|
+
owner: [unit.owner],
|
|
33
|
+
scope: unit.semantic?.scope ?? [],
|
|
34
|
+
meaning: unit.semantic?.meaning === undefined ? [] : [unit.semantic.meaning],
|
|
35
|
+
statement: unit.semantic?.statement === undefined ? [] : [unit.semantic.statement],
|
|
36
|
+
condition: unit.semantic?.condition === undefined ? [] : [unit.semantic.condition],
|
|
37
|
+
failure: unit.semantic?.failure === undefined ? [] : [unit.semantic.failure],
|
|
38
|
+
reason: unit.metadata.reason === undefined ? [] : [unit.metadata.reason],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fieldPointer(unitIndex, field, valueIndex) {
|
|
43
|
+
if (field === "id" || field === "owner") return `/units/${unitIndex}/${field}`;
|
|
44
|
+
if (field === "title" || field === "reason") return `/units/${unitIndex}/metadata/${field}`;
|
|
45
|
+
if (field === "scope") return `/units/${unitIndex}/semantic/scope/${valueIndex}`;
|
|
46
|
+
return `/units/${unitIndex}/semantic/${field}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function portableLocation(mapped, basePath) {
|
|
50
|
+
return { ...mapped, file: portablePath(mapped.file, basePath) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function matchedCandidate(compiled, unit, unitIndex, queryTerms, basePath) {
|
|
54
|
+
const values = fieldValues(unit);
|
|
55
|
+
const termSets = {};
|
|
56
|
+
const matches = [];
|
|
57
|
+
for (const field of FIELD_ORDER) {
|
|
58
|
+
const fieldTerms = new Set();
|
|
59
|
+
values[field].forEach((value, valueIndex) => {
|
|
60
|
+
const orderedTerms = orderedDiscoveryTerms(value);
|
|
61
|
+
const valueTerms = new Set(orderedTerms);
|
|
62
|
+
const terms = queryTerms.filter((term) => valueTerms.has(term));
|
|
63
|
+
if (terms.length === 0) return;
|
|
64
|
+
terms.forEach((term) => fieldTerms.add(term));
|
|
65
|
+
const mapped = compiled.sourceMap.fields[fieldPointer(unitIndex, field, valueIndex)];
|
|
66
|
+
matches.push({ field, terms, location: portableLocation(mapped, basePath), orderedTerms });
|
|
67
|
+
});
|
|
68
|
+
termSets[field] = fieldTerms;
|
|
69
|
+
}
|
|
70
|
+
const all = new Set(FIELD_ORDER.flatMap((field) => [...termSets[field]]));
|
|
71
|
+
if (all.size === 0) return null;
|
|
72
|
+
const idMapped = compiled.sourceMap.fields[`/units/${unitIndex}/id`];
|
|
73
|
+
return {
|
|
74
|
+
candidate: {
|
|
75
|
+
id: unit.id,
|
|
76
|
+
kind: unit.kind,
|
|
77
|
+
lifecycle: unit.lifecycle,
|
|
78
|
+
owner: unit.owner,
|
|
79
|
+
title: unit.metadata.title,
|
|
80
|
+
scope: unit.semantic?.scope ?? [],
|
|
81
|
+
matchedTerms: queryTerms.filter((term) => all.has(term)),
|
|
82
|
+
matches,
|
|
83
|
+
primaryIdLocation: portableLocation(idMapped, basePath),
|
|
84
|
+
},
|
|
85
|
+
counts: {
|
|
86
|
+
identity: new Set([...termSets.id, ...termSets.title]).size,
|
|
87
|
+
primaryMeaning: new Set([...termSets.meaning, ...termSets.statement]).size,
|
|
88
|
+
all: all.size,
|
|
89
|
+
...Object.fromEntries(RANK_FIELDS.map((field) => [field, termSets[field].size])),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function compareCandidates(left, right) {
|
|
95
|
+
return right.counts.identity - left.counts.identity
|
|
96
|
+
|| right.counts.primaryMeaning - left.counts.primaryMeaning
|
|
97
|
+
|| right.counts.all - left.counts.all
|
|
98
|
+
|| RANK_FIELDS.map((field) => right.counts[field] - left.counts[field]).find((value) => value !== 0)
|
|
99
|
+
|| compareText(left.candidate.id, right.candidate.id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function snippetWindow(orderedTerms, queryTerms, maxSnippetTerms) {
|
|
103
|
+
const width = Math.min(maxSnippetTerms, orderedTerms.length);
|
|
104
|
+
const querySet = new Set(queryTerms);
|
|
105
|
+
let start = 0;
|
|
106
|
+
if (width < orderedTerms.length) {
|
|
107
|
+
const frequencies = new Map();
|
|
108
|
+
const add = (term) => {
|
|
109
|
+
if (querySet.has(term)) frequencies.set(term, (frequencies.get(term) ?? 0) + 1);
|
|
110
|
+
};
|
|
111
|
+
const remove = (term) => {
|
|
112
|
+
if (!querySet.has(term)) return;
|
|
113
|
+
const remaining = frequencies.get(term) - 1;
|
|
114
|
+
if (remaining === 0) frequencies.delete(term);
|
|
115
|
+
else frequencies.set(term, remaining);
|
|
116
|
+
};
|
|
117
|
+
for (let index = 0; index < width; index += 1) add(orderedTerms[index]);
|
|
118
|
+
let bestScore = frequencies.size;
|
|
119
|
+
for (let nextStart = 1; nextStart <= orderedTerms.length - width; nextStart += 1) {
|
|
120
|
+
remove(orderedTerms[nextStart - 1]);
|
|
121
|
+
add(orderedTerms[nextStart + width - 1]);
|
|
122
|
+
if (frequencies.size > bestScore) {
|
|
123
|
+
bestScore = frequencies.size;
|
|
124
|
+
start = nextStart;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const end = start + width;
|
|
129
|
+
const terms = orderedTerms.slice(start, end);
|
|
130
|
+
const windowTerms = new Set(terms);
|
|
131
|
+
const matchedTerms = queryTerms.filter((term) => windowTerms.has(term));
|
|
132
|
+
const anchorOffset = terms.findIndex((term) => querySet.has(term));
|
|
133
|
+
return {
|
|
134
|
+
normalization: "NFKC_camel_boundary_lowercase_Unicode_Letter_Number_terms",
|
|
135
|
+
terms,
|
|
136
|
+
anchor: {
|
|
137
|
+
term: terms[anchorOffset],
|
|
138
|
+
fieldTermIndex: start + anchorOffset,
|
|
139
|
+
},
|
|
140
|
+
matchedTerms,
|
|
141
|
+
omittedBeforeTerms: start,
|
|
142
|
+
omittedAfterTerms: orderedTerms.length - end,
|
|
143
|
+
complete: start === 0 && end === orderedTerms.length,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function publicCandidate(ranked, rank, queryTerms, maxSnippetTerms) {
|
|
148
|
+
const { candidate } = ranked;
|
|
149
|
+
return {
|
|
150
|
+
rank,
|
|
151
|
+
id: candidate.id,
|
|
152
|
+
kind: candidate.kind,
|
|
153
|
+
lifecycle: candidate.lifecycle,
|
|
154
|
+
owner: candidate.owner,
|
|
155
|
+
title: candidate.title,
|
|
156
|
+
scope: candidate.scope,
|
|
157
|
+
matchedTerms: candidate.matchedTerms,
|
|
158
|
+
matches: candidate.matches.map(({ field, terms, location, orderedTerms }) => ({
|
|
159
|
+
field,
|
|
160
|
+
terms,
|
|
161
|
+
location,
|
|
162
|
+
snippet: snippetWindow(orderedTerms, queryTerms, maxSnippetTerms),
|
|
163
|
+
})),
|
|
164
|
+
primaryIdLocation: candidate.primaryIdLocation,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function inputLocation(inputPath) {
|
|
169
|
+
const absolute = path.resolve(inputPath);
|
|
170
|
+
const info = await stat(absolute);
|
|
171
|
+
return {
|
|
172
|
+
basePath: info.isDirectory() ? absolute : path.dirname(absolute),
|
|
173
|
+
label: info.isDirectory() ? "." : path.basename(absolute),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function discoveryDiagnostic(location, code, reason, repair) {
|
|
178
|
+
return makeDiagnostic({
|
|
179
|
+
code,
|
|
180
|
+
file: location.label,
|
|
181
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
182
|
+
pointer: "",
|
|
183
|
+
reason,
|
|
184
|
+
repair,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function failedResult(compiled, diagnostics = compiled.diagnostics) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
diagnostics,
|
|
192
|
+
fileCount: compiled.fileCount,
|
|
193
|
+
unitCount: 0,
|
|
194
|
+
discoveryBytes: 0,
|
|
195
|
+
discovery: null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function exactFilterValue(value) {
|
|
200
|
+
return typeof value === "string"
|
|
201
|
+
&& value === value.normalize("NFC")
|
|
202
|
+
&& IDENTIFIER.test(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validateBudgets(options, compiled, location) {
|
|
206
|
+
for (const [name, value] of [["maxCandidates", options.maxCandidates], ["maxSnippetTerms", options.maxSnippetTerms], ["maxBytes", options.maxBytes]]) {
|
|
207
|
+
if (!Number.isSafeInteger(value) || value <= 0) return failedResult(compiled, [discoveryDiagnostic(
|
|
208
|
+
location,
|
|
209
|
+
"AUTH_DISCOVERY_BUDGET",
|
|
210
|
+
`${name} must be one positive safe integer`,
|
|
211
|
+
"provide explicit positive safe-integer discovery budgets",
|
|
212
|
+
)]);
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateFilters(compiled, location, filters) {
|
|
218
|
+
if (filters.kind !== null && !KINDS.has(filters.kind)) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
219
|
+
location,
|
|
220
|
+
"AUTH_DISCOVERY_FILTER_INVALID",
|
|
221
|
+
"kind filter must be exactly definition or rule",
|
|
222
|
+
"provide one admitted exact kind or omit the filter",
|
|
223
|
+
)]) };
|
|
224
|
+
if (filters.lifecycle !== null && !LIFECYCLES.has(filters.lifecycle)) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
225
|
+
location,
|
|
226
|
+
"AUTH_DISCOVERY_FILTER_INVALID",
|
|
227
|
+
"lifecycle filter must be exactly active or removed",
|
|
228
|
+
"provide one admitted exact lifecycle or omit the filter",
|
|
229
|
+
)]) };
|
|
230
|
+
for (const key of ["owner", "scope"]) {
|
|
231
|
+
if (filters[key] !== null && !exactFilterValue(filters[key])) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
232
|
+
location,
|
|
233
|
+
"AUTH_DISCOVERY_FILTER_INVALID",
|
|
234
|
+
`${key} filter must be one exact NFC dotted lowercase identifier`,
|
|
235
|
+
`provide one exact admitted ${key} value or omit the filter`,
|
|
236
|
+
)]) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const owners = new Set(compiled.ir.units.map((unit) => unit.owner));
|
|
240
|
+
const scopes = new Set(compiled.ir.units.flatMap((unit) => unit.semantic?.scope ?? []));
|
|
241
|
+
for (const [key, universe] of [["owner", owners], ["scope", scopes]]) {
|
|
242
|
+
if (filters[key] !== null && !universe.has(filters[key])) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
243
|
+
location,
|
|
244
|
+
"AUTH_DISCOVERY_FILTER_UNKNOWN",
|
|
245
|
+
`${key} filter does not resolve in the complete admitted corpus: ${filters[key]}`,
|
|
246
|
+
`obtain one exact admitted ${key} value; unknown filters are not treated as an empty result`,
|
|
247
|
+
)]) };
|
|
248
|
+
}
|
|
249
|
+
if (filters.scope !== null && (filters.kind === "definition" || filters.lifecycle === "removed")) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
250
|
+
location,
|
|
251
|
+
"AUTH_DISCOVERY_FILTER_CONTRADICTORY",
|
|
252
|
+
"scope filters can select only active rules, but the exact structural filters exclude active rules",
|
|
253
|
+
"remove the contradictory filter or provide a structurally possible exact filter combination",
|
|
254
|
+
)]) };
|
|
255
|
+
|
|
256
|
+
const eligible = compiled.ir.units.filter((unit) => (
|
|
257
|
+
(filters.kind === null || unit.kind === filters.kind)
|
|
258
|
+
&& (filters.owner === null || unit.owner === filters.owner)
|
|
259
|
+
&& (filters.lifecycle === null || unit.lifecycle === filters.lifecycle)
|
|
260
|
+
&& (filters.scope === null || (unit.semantic?.scope ?? []).includes(filters.scope))
|
|
261
|
+
));
|
|
262
|
+
if (eligible.length === 0) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
263
|
+
location,
|
|
264
|
+
"AUTH_DISCOVERY_FILTER_CONTRADICTORY",
|
|
265
|
+
"the exact structural filter intersection contains no admitted unit",
|
|
266
|
+
"remove or correct the contradictory exact filter combination; do not infer authority absence",
|
|
267
|
+
)]) };
|
|
268
|
+
return { eligible };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validatePreview(compiled, location, options) {
|
|
272
|
+
const values = [options.previewDirection, options.relations, options.maxEdges];
|
|
273
|
+
const present = values.filter((value) => value !== null && value !== undefined).length;
|
|
274
|
+
if (present === 0) return { preview: null };
|
|
275
|
+
if (present !== values.length
|
|
276
|
+
|| !DIRECTIONS.has(options.previewDirection)
|
|
277
|
+
|| !Array.isArray(options.relations)
|
|
278
|
+
|| options.relations.length === 0
|
|
279
|
+
|| new Set(options.relations).size !== options.relations.length
|
|
280
|
+
|| options.relations.some((relation) => !RELATION_TYPES.has(relation))
|
|
281
|
+
|| !Number.isSafeInteger(options.maxEdges)
|
|
282
|
+
|| options.maxEdges <= 0) return { result: failedResult(compiled, [discoveryDiagnostic(
|
|
283
|
+
location,
|
|
284
|
+
"AUTH_DISCOVERY_PREVIEW_INVALID",
|
|
285
|
+
"relation preview requires one direction, one non-empty unique closed relation set, and one positive safe-integer maxEdges budget",
|
|
286
|
+
"provide previewDirection, relations, and maxEdges together or omit all three",
|
|
287
|
+
)]) };
|
|
288
|
+
return {
|
|
289
|
+
preview: {
|
|
290
|
+
direction: options.previewDirection,
|
|
291
|
+
relations: [...options.relations].sort(compareText),
|
|
292
|
+
maxEdges: options.maxEdges,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function edgeKey(edge) {
|
|
298
|
+
return `${edge.source}\u0000${edge.type}\u0000${edge.target}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function compareEdge(left, right) {
|
|
302
|
+
return compareText(left.source, right.source)
|
|
303
|
+
|| compareText(left.type, right.type)
|
|
304
|
+
|| compareText(left.target, right.target);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function relationPreview(compiled, basePath, candidates, preview) {
|
|
308
|
+
if (preview === null) return { value: null };
|
|
309
|
+
const snapshot = buildAuthorityGraphSnapshot(compiled, basePath, preview.relations);
|
|
310
|
+
const selected = new Map();
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (preview.direction === "outgoing" || preview.direction === "both") {
|
|
313
|
+
for (const edge of snapshot.outgoing.get(candidate.id)) selected.set(edgeKey(edge), edge);
|
|
314
|
+
}
|
|
315
|
+
if (preview.direction === "incoming" || preview.direction === "both") {
|
|
316
|
+
for (const edge of snapshot.incoming.get(candidate.id)) selected.set(edgeKey(edge), edge);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const edges = [...selected.values()].sort(compareEdge);
|
|
320
|
+
if (edges.length > preview.maxEdges) return {
|
|
321
|
+
requiredEdges: edges.length,
|
|
322
|
+
};
|
|
323
|
+
const nodeIds = new Set(candidates.map((candidate) => candidate.id));
|
|
324
|
+
for (const edge of edges) {
|
|
325
|
+
nodeIds.add(edge.source);
|
|
326
|
+
nodeIds.add(edge.target);
|
|
327
|
+
}
|
|
328
|
+
const nodes = [...nodeIds].sort(compareText).map((id) => snapshot.byId.get(id).node);
|
|
329
|
+
return {
|
|
330
|
+
value: {
|
|
331
|
+
direction: preview.direction,
|
|
332
|
+
relations: preview.relations,
|
|
333
|
+
complete: true,
|
|
334
|
+
roots: candidates.map((candidate) => candidate.id),
|
|
335
|
+
nodes,
|
|
336
|
+
edges,
|
|
337
|
+
counts: {
|
|
338
|
+
roots: candidates.length,
|
|
339
|
+
units: nodes.length,
|
|
340
|
+
edges: edges.length,
|
|
341
|
+
},
|
|
342
|
+
budgets: { maxEdges: preview.maxEdges },
|
|
343
|
+
policy: {
|
|
344
|
+
selection: "complete_direct_authored_edges_incident_to_returned_candidates",
|
|
345
|
+
edgeRepresentation: "canonical_authored_source_to_target",
|
|
346
|
+
duplicateNodesAndEdges: "suppressed",
|
|
347
|
+
rankingEffect: "none",
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function canonicalDiscoveryBytes(discovery) {
|
|
354
|
+
return Buffer.byteLength(JSON.stringify(discovery), "utf8");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function discoverAuthorityPath(inputPath, query, {
|
|
358
|
+
maxCandidates,
|
|
359
|
+
maxSnippetTerms,
|
|
360
|
+
maxBytes,
|
|
361
|
+
kind = null,
|
|
362
|
+
owner = null,
|
|
363
|
+
scope = null,
|
|
364
|
+
lifecycle = null,
|
|
365
|
+
previewDirection = null,
|
|
366
|
+
relations = null,
|
|
367
|
+
maxEdges = null,
|
|
368
|
+
}) {
|
|
369
|
+
const compiled = await compileAuthorityPath(inputPath);
|
|
370
|
+
if (!compiled.ok) return failedResult(compiled);
|
|
371
|
+
const location = await inputLocation(inputPath);
|
|
372
|
+
const budgetFailure = validateBudgets({ maxCandidates, maxSnippetTerms, maxBytes }, compiled, location);
|
|
373
|
+
if (budgetFailure) return budgetFailure;
|
|
374
|
+
const filters = { kind, owner, scope, lifecycle };
|
|
375
|
+
const filtered = validateFilters(compiled, location, filters);
|
|
376
|
+
if (filtered.result) return filtered.result;
|
|
377
|
+
const previewValidation = validatePreview(compiled, location, { previewDirection, relations, maxEdges });
|
|
378
|
+
if (previewValidation.result) return previewValidation.result;
|
|
379
|
+
|
|
380
|
+
const normalizedTerms = normalizeDiscoveryTerms(query);
|
|
381
|
+
if (normalizedTerms.length === 0) return failedResult(compiled, [discoveryDiagnostic(
|
|
382
|
+
location,
|
|
383
|
+
"AUTH_DISCOVERY_QUERY_INVALID",
|
|
384
|
+
"authority discovery query contains no Unicode Letter or Number lexical term",
|
|
385
|
+
"provide one or more lexical terms; semantic inference, translation, fuzzy matching, and fallback queries are not performed",
|
|
386
|
+
)]);
|
|
387
|
+
|
|
388
|
+
const eligible = new Set(filtered.eligible);
|
|
389
|
+
const ranked = compiled.ir.units
|
|
390
|
+
.map((unit, unitIndex) => ({ unit, unitIndex }))
|
|
391
|
+
.filter(({ unit }) => eligible.has(unit))
|
|
392
|
+
.map(({ unit, unitIndex }) => matchedCandidate(compiled, unit, unitIndex, normalizedTerms, location.basePath))
|
|
393
|
+
.filter((candidate) => candidate !== null)
|
|
394
|
+
.sort(compareCandidates);
|
|
395
|
+
if (ranked.length === 0) return failedResult(compiled, [discoveryDiagnostic(
|
|
396
|
+
location,
|
|
397
|
+
"AUTH_DISCOVERY_NOT_FOUND",
|
|
398
|
+
"lexical discovery found no candidate in the exact eligible universe; this does not prove that authority does not exist",
|
|
399
|
+
"rewrite the lexical query, revise exact filters, obtain an exact authority ID, or use an external semantic host; do not infer absence",
|
|
400
|
+
)]);
|
|
401
|
+
|
|
402
|
+
const candidates = ranked.slice(0, maxCandidates).map((entry, index) => publicCandidate(entry, index + 1, normalizedTerms, maxSnippetTerms));
|
|
403
|
+
const previewResult = relationPreview(compiled, location.basePath, candidates, previewValidation.preview);
|
|
404
|
+
if (previewResult.requiredEdges !== undefined) return failedResult(compiled, [discoveryDiagnostic(
|
|
405
|
+
location,
|
|
406
|
+
"AUTH_DISCOVERY_PREVIEW_BUDGET",
|
|
407
|
+
`complete direct relation preview requires ${previewResult.requiredEdges} authored edges but max-edges is ${maxEdges}`,
|
|
408
|
+
"increase the explicit edge budget; relation preview edges are not silently removed",
|
|
409
|
+
)]);
|
|
410
|
+
|
|
411
|
+
const discovery = {
|
|
412
|
+
format: DISCOVERY_FORMAT,
|
|
413
|
+
query: { text: query, normalizedTerms },
|
|
414
|
+
filters,
|
|
415
|
+
counts: {
|
|
416
|
+
corpusUnits: compiled.ir.units.length,
|
|
417
|
+
eligibleUnits: filtered.eligible.length,
|
|
418
|
+
matchedUnits: ranked.length,
|
|
419
|
+
returnedCandidates: candidates.length,
|
|
420
|
+
},
|
|
421
|
+
truncated: ranked.length > candidates.length,
|
|
422
|
+
absenceProven: false,
|
|
423
|
+
candidates,
|
|
424
|
+
relationPreview: previewResult.value,
|
|
425
|
+
budgets: {
|
|
426
|
+
maxCandidates,
|
|
427
|
+
maxSnippetTerms,
|
|
428
|
+
maxEdges: previewValidation.preview?.maxEdges ?? null,
|
|
429
|
+
maxBytes,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
const discoveryBytes = canonicalDiscoveryBytes(discovery);
|
|
433
|
+
if (discoveryBytes > maxBytes) return failedResult(compiled, [discoveryDiagnostic(
|
|
434
|
+
location,
|
|
435
|
+
"AUTH_DISCOVERY_BUDGET",
|
|
436
|
+
`complete bounded discovery payload requires ${discoveryBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
|
|
437
|
+
"increase the explicit byte budget; candidates, snippets, and relation preview are not silently removed",
|
|
438
|
+
)]);
|
|
439
|
+
return {
|
|
440
|
+
ok: true,
|
|
441
|
+
diagnostics: [],
|
|
442
|
+
fileCount: compiled.fileCount,
|
|
443
|
+
unitCount: candidates.length,
|
|
444
|
+
discoveryBytes,
|
|
445
|
+
discovery,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { compareText, createLocator, makeDiagnostic, portablePath, REPAIRS, sortDiagnostics } from "./diagnostics.mjs";
|
|
5
|
+
import { parseMarkdownAuthority, stringifyCanonicalMarkdown } from "./source-markdown.mjs";
|
|
6
|
+
import { loadAuthorityContract, parseYamlAuthority, stringifyCanonicalYaml } from "./source-yaml.mjs";
|
|
7
|
+
import { validateFormatStructure } from "./validate.mjs";
|
|
8
|
+
|
|
9
|
+
export class AuthorityInputError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "AuthorityInputError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function profileFor(filePath) {
|
|
17
|
+
if (filePath.endsWith(".authority.yaml")) return "yaml";
|
|
18
|
+
if (filePath.endsWith(".authority.md")) return "markdown";
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function collectDirectory(current, files) {
|
|
23
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
24
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const absolutePath = path.join(current, entry.name);
|
|
27
|
+
if (entry.isSymbolicLink()) throw new AuthorityInputError(`authority input refuses symbolic link: ${absolutePath}`);
|
|
28
|
+
if (entry.isDirectory()) await collectDirectory(absolutePath, files);
|
|
29
|
+
else if (entry.isFile()) {
|
|
30
|
+
if (!profileFor(absolutePath)) throw new AuthorityInputError(`authority input contains unsupported file: ${absolutePath}`);
|
|
31
|
+
files.push(absolutePath);
|
|
32
|
+
} else throw new AuthorityInputError(`authority input contains unsupported filesystem entry: ${absolutePath}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function collectAuthorityFiles(inputPath, options = {}) {
|
|
37
|
+
const absolutePath = path.resolve(process.cwd(), inputPath);
|
|
38
|
+
let info;
|
|
39
|
+
try {
|
|
40
|
+
info = await lstat(absolutePath);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new AuthorityInputError(`authority input is not readable: ${absolutePath}: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (info.isSymbolicLink()) throw new AuthorityInputError(`authority input refuses symbolic link: ${absolutePath}`);
|
|
45
|
+
if (options.singleFile && !info.isFile()) throw new AuthorityInputError("authority fmt requires one regular canonical source file");
|
|
46
|
+
const files = [];
|
|
47
|
+
if (info.isFile()) {
|
|
48
|
+
if (!profileFor(absolutePath)) throw new AuthorityInputError(`unsupported authority source suffix: ${absolutePath}`);
|
|
49
|
+
files.push(absolutePath);
|
|
50
|
+
} else if (info.isDirectory()) await collectDirectory(absolutePath, files);
|
|
51
|
+
else throw new AuthorityInputError(`unsupported authority input type: ${absolutePath}`);
|
|
52
|
+
if (files.length === 0) throw new AuthorityInputError(`authority input contains no canonical source files: ${absolutePath}`);
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function parseAuthorityFile(absolutePath) {
|
|
57
|
+
const bytes = await readFile(absolutePath);
|
|
58
|
+
const profile = profileFor(absolutePath);
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
62
|
+
} catch {
|
|
63
|
+
const file = portablePath(absolutePath);
|
|
64
|
+
const locator = createLocator("");
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
file,
|
|
68
|
+
locator,
|
|
69
|
+
diagnostics: [makeDiagnostic({
|
|
70
|
+
code: "AUTH_SYNTAX_UNSUPPORTED",
|
|
71
|
+
file,
|
|
72
|
+
range: locator.range(0),
|
|
73
|
+
reason: "authority source bytes are not valid UTF-8",
|
|
74
|
+
repair: "rewrite the file as valid UTF-8 without changing product semantics",
|
|
75
|
+
})],
|
|
76
|
+
locations: new Map(),
|
|
77
|
+
keyLocations: new Map(),
|
|
78
|
+
insertionPoints: new Map(),
|
|
79
|
+
data: null,
|
|
80
|
+
profile,
|
|
81
|
+
isDocument: true,
|
|
82
|
+
units: [],
|
|
83
|
+
sourceText: "",
|
|
84
|
+
inputBytes: bytes.length,
|
|
85
|
+
absolutePath,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const source = profile === "yaml"
|
|
89
|
+
? parseYamlAuthority(text, absolutePath)
|
|
90
|
+
: parseMarkdownAuthority(text, absolutePath);
|
|
91
|
+
const document = profile === "yaml"
|
|
92
|
+
? source
|
|
93
|
+
: { ...source, isDocument: true, units: source.data ? [source] : [] };
|
|
94
|
+
return { ...document, inputBytes: bytes.length, absolutePath };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function parseAuthorityPath(inputPath, options = {}) {
|
|
98
|
+
const contract = await loadAuthorityContract();
|
|
99
|
+
const files = await collectAuthorityFiles(inputPath, options);
|
|
100
|
+
const documents = [];
|
|
101
|
+
const sources = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const document = await parseAuthorityFile(file);
|
|
104
|
+
documents.push(document);
|
|
105
|
+
sources.push(...document.units.map((source) => ({ ...source, absolutePath: document.absolutePath })));
|
|
106
|
+
}
|
|
107
|
+
return { contract, files, documents, sources };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function canonicalText(document) {
|
|
111
|
+
return document.profile === "yaml"
|
|
112
|
+
? stringifyCanonicalYaml(document.data, { container: true })
|
|
113
|
+
: stringifyCanonicalMarkdown(document.data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formattingDiagnostics(documents, contract) {
|
|
117
|
+
const diagnostics = [];
|
|
118
|
+
for (const document of documents) {
|
|
119
|
+
const structure = validateFormatStructure(document, contract);
|
|
120
|
+
if (structure.length > 0) {
|
|
121
|
+
diagnostics.push(...structure);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const canonical = canonicalText(document);
|
|
125
|
+
if (canonical !== document.sourceText) {
|
|
126
|
+
diagnostics.push(makeDiagnostic({
|
|
127
|
+
code: "AUTH_FORMAT_DRIFT",
|
|
128
|
+
file: document.file,
|
|
129
|
+
range: document.locations.get("") ?? document.locator.range(0, document.sourceText.length),
|
|
130
|
+
reason: "authority source is not byte-canonical",
|
|
131
|
+
repair: REPAIRS.format,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return sortDiagnostics(diagnostics);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function atomicWrite(filePath, content) {
|
|
139
|
+
const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
140
|
+
try {
|
|
141
|
+
await writeFile(temporary, content, "utf8");
|
|
142
|
+
await rename(temporary, filePath);
|
|
143
|
+
} finally {
|
|
144
|
+
await rm(temporary, { force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function formatAuthorityFile(inputPath, options = {}) {
|
|
149
|
+
const { contract, documents } = await parseAuthorityPath(inputPath, { singleFile: true });
|
|
150
|
+
const document = documents[0];
|
|
151
|
+
const diagnostics = validateFormatStructure(document, contract);
|
|
152
|
+
if (diagnostics.length > 0) return { ok: false, changed: false, diagnostics, fileCount: 1 };
|
|
153
|
+
const canonical = canonicalText(document);
|
|
154
|
+
const changed = canonical !== document.sourceText;
|
|
155
|
+
if (options.check && changed) return {
|
|
156
|
+
ok: false,
|
|
157
|
+
changed: true,
|
|
158
|
+
diagnostics: [makeDiagnostic({
|
|
159
|
+
code: "AUTH_FORMAT_DRIFT",
|
|
160
|
+
file: document.file,
|
|
161
|
+
range: document.locations.get("") ?? document.locator.range(0, document.sourceText.length),
|
|
162
|
+
reason: "authority source is not byte-canonical",
|
|
163
|
+
repair: REPAIRS.format,
|
|
164
|
+
})],
|
|
165
|
+
fileCount: 1,
|
|
166
|
+
};
|
|
167
|
+
if (!options.check && changed) await atomicWrite(document.absolutePath ?? path.resolve(process.cwd(), inputPath), canonical);
|
|
168
|
+
return { ok: true, changed, diagnostics: [], fileCount: 1 };
|
|
169
|
+
}
|