@nimiplatform/nimi-coding 0.3.0 → 0.4.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 +27 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +285 -41
- package/README.zh-CN.md +294 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +849 -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 +173 -0
- package/cli/dev/check-product-conformance.mjs +917 -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 +490 -0
- package/cli/lib/authority/closed-sets.mjs +646 -0
- package/cli/lib/authority/compile.mjs +110 -0
- package/cli/lib/authority/consumers.mjs +470 -0
- package/cli/lib/authority/diagnostics.mjs +87 -0
- package/cli/lib/authority/diff.mjs +287 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/evidence-snapshot.mjs +1 -0
- package/cli/lib/authority/evidence.mjs +1432 -0
- package/cli/lib/authority/format.mjs +197 -0
- package/cli/lib/authority/git-snapshot.mjs +1065 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +252 -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 +222 -0
- package/cli/lib/authority/sarif.mjs +181 -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 +312 -0
- package/cli/lib/authority/surface.mjs +79 -0
- package/cli/lib/authority/terms.mjs +421 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +308 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +103 -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-evidence-bindings.schema.yaml +53 -0
- package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
- package/contracts/authority-impact-dispositions.schema.yaml +13 -0
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +64 -0
- package/contracts/authority-verifier-bindings.schema.yaml +20 -0
- package/methodology/authority-authoring.yaml +144 -0
- package/methodology/core.yaml +31 -26
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +85 -0
- package/spec/authority-authoring.yaml +150 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +70 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-evidence.yaml +200 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +84 -0
- package/spec/authority-review.yaml +89 -0
- package/spec/authority-terms.yaml +59 -0
- package/spec/product-scope.yaml +36 -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 -124
- 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 @@
|
|
|
1
|
+
export { withGitEvidenceSnapshot } from "./git-snapshot.mjs";
|