@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,351 @@
|
|
|
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
|
+
|
|
8
|
+
const GRAPH_FORMAT = "nimicoding.authority-graph/v1";
|
|
9
|
+
|
|
10
|
+
async function inputLocation(inputPath) {
|
|
11
|
+
const absolute = path.resolve(inputPath);
|
|
12
|
+
const info = await stat(absolute);
|
|
13
|
+
return {
|
|
14
|
+
basePath: info.isDirectory() ? absolute : path.dirname(absolute),
|
|
15
|
+
label: info.isDirectory() ? "." : path.basename(absolute),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function portableLocation(mapped, basePath) {
|
|
20
|
+
return {
|
|
21
|
+
file: portablePath(mapped.file, basePath),
|
|
22
|
+
range: mapped.range,
|
|
23
|
+
sourcePointer: mapped.sourcePointer,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function diagnostic(location, code, id, reason, repair, mapped = null) {
|
|
28
|
+
return makeDiagnostic({
|
|
29
|
+
code,
|
|
30
|
+
file: mapped ? portablePath(mapped.file, location.basePath) : location.label,
|
|
31
|
+
range: mapped?.range ?? { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
32
|
+
pointer: mapped?.sourcePointer ?? "/id",
|
|
33
|
+
reason: `${reason}: ${id}`,
|
|
34
|
+
repair,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function failedResult(compiled, diagnostics = compiled.diagnostics) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
diagnostics,
|
|
42
|
+
fileCount: compiled.fileCount,
|
|
43
|
+
unitCount: 0,
|
|
44
|
+
graphBytes: 0,
|
|
45
|
+
graph: null,
|
|
46
|
+
partial: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function edgeKey(edge) {
|
|
51
|
+
return `${edge.source}\u0000${edge.type}\u0000${edge.target}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareEdge(left, right) {
|
|
55
|
+
return compareText(left.source, right.source)
|
|
56
|
+
|| compareText(left.type, right.type)
|
|
57
|
+
|| compareText(left.target, right.target);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareStep(left, right) {
|
|
61
|
+
return compareText(left.type, right.type)
|
|
62
|
+
|| compareText(left.traversal, right.traversal)
|
|
63
|
+
|| compareText(left.source, right.source)
|
|
64
|
+
|| compareText(left.target, right.target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function compareStepSequences(left, right) {
|
|
68
|
+
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
|
69
|
+
const compared = compareStep(left[index], right[index]);
|
|
70
|
+
if (compared !== 0) return compared;
|
|
71
|
+
}
|
|
72
|
+
return left.length - right.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildAuthorityGraphSnapshot(compiled, basePath, relations) {
|
|
76
|
+
const selectedTypes = new Set(relations);
|
|
77
|
+
const byId = new Map();
|
|
78
|
+
const outgoing = new Map();
|
|
79
|
+
const incoming = new Map();
|
|
80
|
+
const edges = [];
|
|
81
|
+
|
|
82
|
+
compiled.ir.units.forEach((unit, unitIndex) => {
|
|
83
|
+
const idMap = compiled.sourceMap.fields[`/units/${unitIndex}/id`];
|
|
84
|
+
const node = {
|
|
85
|
+
id: unit.id,
|
|
86
|
+
kind: unit.kind,
|
|
87
|
+
owner: unit.owner,
|
|
88
|
+
lifecycle: unit.lifecycle,
|
|
89
|
+
scope: unit.lifecycle === "active" && unit.kind === "rule" ? [...unit.semantic.scope] : [],
|
|
90
|
+
location: portableLocation(idMap, basePath),
|
|
91
|
+
};
|
|
92
|
+
byId.set(unit.id, { unit, unitIndex, idMap, node });
|
|
93
|
+
outgoing.set(unit.id, []);
|
|
94
|
+
incoming.set(unit.id, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
compiled.ir.units.forEach((unit, unitIndex) => {
|
|
98
|
+
unit.relations.forEach((relation, relationIndex) => {
|
|
99
|
+
if (!selectedTypes.has(relation.type)) return;
|
|
100
|
+
const target = byId.get(relation.target);
|
|
101
|
+
const value = {
|
|
102
|
+
source: unit.id,
|
|
103
|
+
type: relation.type,
|
|
104
|
+
target: relation.target,
|
|
105
|
+
sourceLocation: portableLocation(compiled.sourceMap.fields[`/units/${unitIndex}/id`], basePath),
|
|
106
|
+
relationLocation: portableLocation(compiled.sourceMap.fields[`/units/${unitIndex}/relations/${relationIndex}/target`], basePath),
|
|
107
|
+
targetLocation: target.node.location,
|
|
108
|
+
};
|
|
109
|
+
edges.push(value);
|
|
110
|
+
outgoing.get(value.source).push(value);
|
|
111
|
+
incoming.get(value.target).push(value);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
edges.sort(compareEdge);
|
|
115
|
+
for (const adjacency of [...outgoing.values(), ...incoming.values()]) adjacency.sort(compareEdge);
|
|
116
|
+
return { byId, outgoing, incoming, edges };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function adjacent(snapshot, id, direction) {
|
|
120
|
+
const values = [];
|
|
121
|
+
if (direction === "outgoing" || direction === "both") {
|
|
122
|
+
for (const edge of snapshot.outgoing.get(id)) values.push({ edge, next: edge.target, traversal: "forward" });
|
|
123
|
+
}
|
|
124
|
+
if (direction === "incoming" || direction === "both") {
|
|
125
|
+
for (const edge of snapshot.incoming.get(id)) values.push({ edge, next: edge.source, traversal: "reverse" });
|
|
126
|
+
}
|
|
127
|
+
const deduplicated = new Map();
|
|
128
|
+
for (const value of values) deduplicated.set(`${edgeKey(value.edge)}\u0000${value.traversal}`, value);
|
|
129
|
+
return [...deduplicated.values()].sort((left, right) => compareStep(
|
|
130
|
+
{ ...left.edge, traversal: left.traversal },
|
|
131
|
+
{ ...right.edge, traversal: right.traversal },
|
|
132
|
+
));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function graphBase(operation, query, nodes, edges, policy, counts, budgets, extra = {}) {
|
|
136
|
+
return {
|
|
137
|
+
format: GRAPH_FORMAT,
|
|
138
|
+
operation,
|
|
139
|
+
query,
|
|
140
|
+
complete: true,
|
|
141
|
+
...extra,
|
|
142
|
+
nodes: [...nodes].sort((left, right) => compareText(left.id, right.id)),
|
|
143
|
+
edges: [...edges].sort(compareEdge),
|
|
144
|
+
policy,
|
|
145
|
+
counts,
|
|
146
|
+
budgets,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function budgetFailure(compiled, location, id, reason, mapped) {
|
|
151
|
+
return failedResult(compiled, [diagnostic(
|
|
152
|
+
location,
|
|
153
|
+
"AUTH_GRAPH_BUDGET",
|
|
154
|
+
id,
|
|
155
|
+
reason,
|
|
156
|
+
"increase the explicit graph budget; partial graph results are forbidden",
|
|
157
|
+
mapped,
|
|
158
|
+
)]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function finish(compiled, graph, location, root, traversal) {
|
|
162
|
+
const { maxUnits, maxEdges, maxBytes } = graph.budgets;
|
|
163
|
+
if (traversal.units > maxUnits) return budgetFailure(
|
|
164
|
+
compiled,
|
|
165
|
+
location,
|
|
166
|
+
root.unit.id,
|
|
167
|
+
`complete graph proof requires ${traversal.units} units but max-units is ${maxUnits}`,
|
|
168
|
+
root.idMap,
|
|
169
|
+
);
|
|
170
|
+
if (traversal.edges > maxEdges) return budgetFailure(
|
|
171
|
+
compiled,
|
|
172
|
+
location,
|
|
173
|
+
root.unit.id,
|
|
174
|
+
`complete graph proof requires ${traversal.edges} authored edges but max-edges is ${maxEdges}`,
|
|
175
|
+
root.idMap,
|
|
176
|
+
);
|
|
177
|
+
const graphBytes = Buffer.byteLength(JSON.stringify(graph), "utf8");
|
|
178
|
+
if (graphBytes > maxBytes) return budgetFailure(
|
|
179
|
+
compiled,
|
|
180
|
+
location,
|
|
181
|
+
root.unit.id,
|
|
182
|
+
`complete graph requires ${graphBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
|
|
183
|
+
root.idMap,
|
|
184
|
+
);
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
diagnostics: [],
|
|
188
|
+
fileCount: compiled.fileCount,
|
|
189
|
+
unitCount: graph.nodes.length,
|
|
190
|
+
graphBytes,
|
|
191
|
+
graph,
|
|
192
|
+
partial: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function admittedGraph(inputPath, ids, relations) {
|
|
197
|
+
const compiled = await compileAuthorityPath(inputPath);
|
|
198
|
+
if (!compiled.ok) return { result: failedResult(compiled) };
|
|
199
|
+
const location = await inputLocation(inputPath);
|
|
200
|
+
const snapshot = buildAuthorityGraphSnapshot(compiled, location.basePath, relations);
|
|
201
|
+
for (const [role, id] of ids) {
|
|
202
|
+
if (!snapshot.byId.has(id)) return { result: failedResult(compiled, [diagnostic(
|
|
203
|
+
location,
|
|
204
|
+
"AUTH_QUERY_NOT_FOUND",
|
|
205
|
+
id,
|
|
206
|
+
`${role} authority identity does not resolve`,
|
|
207
|
+
"use an exact admitted authority ID; do not infer a near match",
|
|
208
|
+
)]) };
|
|
209
|
+
}
|
|
210
|
+
return { compiled, location, snapshot };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function refsAuthorityPath(inputPath, id, { direction, relations, maxUnits, maxEdges, maxBytes }) {
|
|
214
|
+
const admitted = await admittedGraph(inputPath, [["reference root", id]], relations);
|
|
215
|
+
if (admitted.result) return admitted.result;
|
|
216
|
+
const { compiled, location, snapshot } = admitted;
|
|
217
|
+
const root = snapshot.byId.get(id);
|
|
218
|
+
const selected = new Map();
|
|
219
|
+
for (const value of adjacent(snapshot, id, direction)) selected.set(edgeKey(value.edge), value.edge);
|
|
220
|
+
const edges = [...selected.values()];
|
|
221
|
+
const nodeIds = new Set([id]);
|
|
222
|
+
for (const edge of edges) { nodeIds.add(edge.source); nodeIds.add(edge.target); }
|
|
223
|
+
const nodes = [...nodeIds].map((nodeId) => snapshot.byId.get(nodeId).node);
|
|
224
|
+
const traversal = { units: nodes.length, edges: edges.length };
|
|
225
|
+
const budgets = { maxUnits, maxEdges, maxBytes };
|
|
226
|
+
const graph = graphBase("refs", { id, direction, relations }, nodes, edges, {
|
|
227
|
+
edgeRepresentation: "canonical_authored_source_to_target",
|
|
228
|
+
selection: "direct_only",
|
|
229
|
+
duplicateEdges: "suppressed",
|
|
230
|
+
ordering: { nodes: "id", edges: ["source", "type", "target"] },
|
|
231
|
+
}, {
|
|
232
|
+
returned: { units: nodes.length, edges: edges.length },
|
|
233
|
+
traversal,
|
|
234
|
+
}, budgets);
|
|
235
|
+
return finish(compiled, graph, location, root, traversal);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function pathAdjacency(snapshot, id, traversal) {
|
|
239
|
+
return adjacent(snapshot, id, traversal === "directed" ? "outgoing" : "both");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function pathAuthorityPath(inputPath, fromId, toId, { traversal: traversalMode, relations, maxHops, maxUnits, maxEdges, maxBytes }) {
|
|
243
|
+
const admitted = await admittedGraph(inputPath, [["path source", fromId], ["path target", toId]], relations);
|
|
244
|
+
if (admitted.result) return admitted.result;
|
|
245
|
+
const { compiled, location, snapshot } = admitted;
|
|
246
|
+
const root = snapshot.byId.get(fromId);
|
|
247
|
+
const visitedDepth = new Map([[fromId, 0]]);
|
|
248
|
+
const proofEdges = new Set();
|
|
249
|
+
let layer = new Map([[fromId, []]]);
|
|
250
|
+
let witness = fromId === toId ? [] : null;
|
|
251
|
+
let exhausted = false;
|
|
252
|
+
|
|
253
|
+
for (let depth = 0; witness === null && depth < maxHops && layer.size > 0; depth += 1) {
|
|
254
|
+
const next = new Map();
|
|
255
|
+
for (const id of [...layer.keys()].sort(compareText)) {
|
|
256
|
+
const prefix = layer.get(id);
|
|
257
|
+
for (const value of pathAdjacency(snapshot, id, traversalMode)) {
|
|
258
|
+
proofEdges.add(edgeKey(value.edge));
|
|
259
|
+
const priorDepth = visitedDepth.get(value.next);
|
|
260
|
+
if (priorDepth !== undefined && priorDepth < depth + 1) continue;
|
|
261
|
+
const candidate = [...prefix, { source: value.edge.source, type: value.edge.type, target: value.edge.target, traversal: value.traversal }];
|
|
262
|
+
const prior = next.get(value.next);
|
|
263
|
+
if (!prior || compareStepSequences(candidate, prior) < 0) next.set(value.next, candidate);
|
|
264
|
+
if (priorDepth === undefined) visitedDepth.set(value.next, depth + 1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (next.has(toId)) witness = next.get(toId);
|
|
268
|
+
if (next.size === 0) exhausted = true;
|
|
269
|
+
layer = next;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (witness === null && !exhausted && layer.size > 0) {
|
|
273
|
+
let unchecked = false;
|
|
274
|
+
for (const id of [...layer.keys()].sort(compareText)) {
|
|
275
|
+
for (const value of pathAdjacency(snapshot, id, traversalMode)) {
|
|
276
|
+
proofEdges.add(edgeKey(value.edge));
|
|
277
|
+
if (!visitedDepth.has(value.next)) unchecked = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (unchecked) return budgetFailure(
|
|
281
|
+
compiled,
|
|
282
|
+
location,
|
|
283
|
+
fromId,
|
|
284
|
+
`complete path result requires traversing beyond max-hops ${maxHops}`,
|
|
285
|
+
root.idMap,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const found = witness !== null;
|
|
290
|
+
const pathEdgeKeys = new Set((witness ?? []).map((step) => `${step.source}\u0000${step.type}\u0000${step.target}`));
|
|
291
|
+
const edges = snapshot.edges.filter((edge) => pathEdgeKeys.has(edgeKey(edge)));
|
|
292
|
+
const pathNodeIds = found
|
|
293
|
+
? [fromId, ...(witness ?? []).map((step) => step.traversal === "forward" ? step.target : step.source)]
|
|
294
|
+
: [fromId, toId];
|
|
295
|
+
const nodes = [...new Set(pathNodeIds)].map((id) => snapshot.byId.get(id).node);
|
|
296
|
+
const traversal = { units: new Set([...visitedDepth.keys(), toId]).size, edges: proofEdges.size };
|
|
297
|
+
const budgets = { maxHops, maxUnits, maxEdges, maxBytes };
|
|
298
|
+
const graph = graphBase("path", { fromId, toId, traversal: traversalMode, relations }, nodes, edges, {
|
|
299
|
+
edgeRepresentation: "canonical_authored_source_to_target",
|
|
300
|
+
traversal: traversalMode === "directed" ? "authored_forward_only" : "incidence_forward_or_reverse",
|
|
301
|
+
reverseStepClaim: "topology_incidence_only_not_semantic_dependency",
|
|
302
|
+
selection: "shortest_hops_then_lexical_minimum_complete_step_tuple_sequence",
|
|
303
|
+
stepTuple: ["type", "traversal", "authored_source", "authored_target"],
|
|
304
|
+
ordering: { nodes: "id", edges: ["source", "type", "target"] },
|
|
305
|
+
}, {
|
|
306
|
+
returned: { units: nodes.length, edges: edges.length, hops: witness?.length ?? 0 },
|
|
307
|
+
traversal,
|
|
308
|
+
}, budgets, { found, steps: witness ?? [] });
|
|
309
|
+
return finish(compiled, graph, location, root, traversal);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function subgraphAuthorityPath(inputPath, id, { direction, relations, depth, maxUnits, maxEdges, maxBytes }) {
|
|
313
|
+
const admitted = await admittedGraph(inputPath, [["subgraph root", id]], relations);
|
|
314
|
+
if (admitted.result) return admitted.result;
|
|
315
|
+
const { compiled, location, snapshot } = admitted;
|
|
316
|
+
const root = snapshot.byId.get(id);
|
|
317
|
+
const distances = new Map([[id, 0]]);
|
|
318
|
+
const proofEdges = new Set();
|
|
319
|
+
let layer = [id];
|
|
320
|
+
for (let currentDepth = 0; currentDepth < depth && layer.length > 0; currentDepth += 1) {
|
|
321
|
+
const next = new Set();
|
|
322
|
+
for (const current of [...layer].sort(compareText)) {
|
|
323
|
+
for (const value of adjacent(snapshot, current, direction)) {
|
|
324
|
+
proofEdges.add(edgeKey(value.edge));
|
|
325
|
+
if (!distances.has(value.next)) {
|
|
326
|
+
distances.set(value.next, currentDepth + 1);
|
|
327
|
+
next.add(value.next);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
layer = [...next].sort(compareText);
|
|
332
|
+
}
|
|
333
|
+
const nodeIds = new Set(distances.keys());
|
|
334
|
+
const edges = snapshot.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target));
|
|
335
|
+
for (const edge of edges) proofEdges.add(edgeKey(edge));
|
|
336
|
+
const nodes = [...nodeIds].map((nodeId) => snapshot.byId.get(nodeId).node);
|
|
337
|
+
const traversal = { units: nodes.length, edges: proofEdges.size };
|
|
338
|
+
const budgets = { depth, maxUnits, maxEdges, maxBytes };
|
|
339
|
+
const graph = graphBase("subgraph", { id, direction, relations, depth }, nodes, edges, {
|
|
340
|
+
edgeRepresentation: "canonical_authored_source_to_target",
|
|
341
|
+
traversal: "deterministic_breadth_first",
|
|
342
|
+
selection: "complete_selected_graph_within_depth",
|
|
343
|
+
duplicateNodesAndEdges: "suppressed",
|
|
344
|
+
cycleSafe: true,
|
|
345
|
+
ordering: { nodes: "id", edges: ["source", "type", "target"] },
|
|
346
|
+
}, {
|
|
347
|
+
returned: { units: nodes.length, edges: edges.length },
|
|
348
|
+
traversal,
|
|
349
|
+
}, budgets);
|
|
350
|
+
return finish(compiled, graph, location, root, traversal);
|
|
351
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { lstat, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
|
|
6
|
+
import { compareText, makeDiagnostic, portablePath, sortDiagnostics } from "./diagnostics.mjs";
|
|
7
|
+
import { diffAuthorityPaths } from "./diff.mjs";
|
|
8
|
+
|
|
9
|
+
const contractUrl = new URL("../../../contracts/authority-impact-dispositions.schema.yaml", import.meta.url);
|
|
10
|
+
const IMPACT_FORMAT = "nimicoding.authority-impact/v1";
|
|
11
|
+
const IMPACT_RELATIONS = new Set(["applies_to", "supersedes"]);
|
|
12
|
+
|
|
13
|
+
async function rootFor(inputPath) {
|
|
14
|
+
const absolute = path.resolve(inputPath);
|
|
15
|
+
const info = await stat(absolute);
|
|
16
|
+
return info.isDirectory() ? absolute : path.dirname(absolute);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function impactBudgetDiagnostic(requiredBytes, maxBytes) {
|
|
20
|
+
return makeDiagnostic({
|
|
21
|
+
code: "AUTH_IMPACT_BUDGET",
|
|
22
|
+
file: ".",
|
|
23
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
24
|
+
pointer: "",
|
|
25
|
+
reason: `complete semantic impact payload requires ${requiredBytes} UTF-8 bytes but max-bytes is ${maxBytes}`,
|
|
26
|
+
repair: "increase the explicit byte budget; partial diff or impact output is forbidden",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function canonicalImpactBytes(diff, impact) {
|
|
31
|
+
return Buffer.byteLength(JSON.stringify({ diff, impact }), "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function overBudget(diffResult, impact, maxBytes) {
|
|
35
|
+
const payloadBytes = canonicalImpactBytes(diffResult.diff, impact);
|
|
36
|
+
if (maxBytes === null || payloadBytes <= maxBytes) return { payloadBytes, failure: null };
|
|
37
|
+
return {
|
|
38
|
+
payloadBytes,
|
|
39
|
+
failure: {
|
|
40
|
+
ok: false,
|
|
41
|
+
diagnostics: [impactBudgetDiagnostic(payloadBytes, maxBytes)],
|
|
42
|
+
fileCount: diffResult.fileCount,
|
|
43
|
+
unitCount: 0,
|
|
44
|
+
payloadBytes,
|
|
45
|
+
diff: null,
|
|
46
|
+
impact: null,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function invalidDisposition(file, reason) {
|
|
52
|
+
return makeDiagnostic({
|
|
53
|
+
code: "AUTH_IMPACT_DISPOSITION_INVALID",
|
|
54
|
+
file: path.basename(file),
|
|
55
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
|
|
56
|
+
pointer: "",
|
|
57
|
+
reason,
|
|
58
|
+
repair: "declare each exact impacted rule consumer and test disposition with addressed status and explicit evidence",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactObject(value, fields) {
|
|
63
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
64
|
+
&& Object.keys(value).every((key) => fields.includes(key));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadDispositions(file, label = file) {
|
|
68
|
+
const absolute = path.resolve(file);
|
|
69
|
+
let bytes;
|
|
70
|
+
try {
|
|
71
|
+
const info = await lstat(absolute);
|
|
72
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
73
|
+
return { ok: false, diagnostics: [invalidDisposition(label, "dispositions must be one regular non-symlink YAML file")] };
|
|
74
|
+
}
|
|
75
|
+
bytes = await readFile(absolute);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { ok: false, diagnostics: [invalidDisposition(label, `disposition file is not readable: ${error.message}`)] };
|
|
78
|
+
}
|
|
79
|
+
const contractText = await readFile(contractUrl, "utf8");
|
|
80
|
+
let text;
|
|
81
|
+
try {
|
|
82
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
83
|
+
} catch {
|
|
84
|
+
return { ok: false, diagnostics: [invalidDisposition(label, "disposition bytes are not valid UTF-8")] };
|
|
85
|
+
}
|
|
86
|
+
const document = YAML.parseDocument(text, { uniqueKeys: true, version: "1.2" });
|
|
87
|
+
if (document.errors.length > 0) return { ok: false, diagnostics: [invalidDisposition(label, document.errors[0].message)] };
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = document.toJS({ maxAliasCount: 0 });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return { ok: false, diagnostics: [invalidDisposition(label, error.message)] };
|
|
93
|
+
}
|
|
94
|
+
const contract = YAML.parse(contractText);
|
|
95
|
+
if (!exactObject(data, contract.fields.top) || data.format !== contract.format || !Array.isArray(data.rules)) {
|
|
96
|
+
return { ok: false, diagnostics: [invalidDisposition(label, "dispositions require the exact format and rules list")] };
|
|
97
|
+
}
|
|
98
|
+
const entries = new Map();
|
|
99
|
+
const ruleIds = new Set();
|
|
100
|
+
const identifier = new RegExp(contract.identifier_pattern);
|
|
101
|
+
for (const rule of data.rules) {
|
|
102
|
+
if (!exactObject(rule, contract.fields.rule) || !identifier.test(rule.id) || !Array.isArray(rule.consumers) || !exactObject(rule.test, contract.fields.test)) {
|
|
103
|
+
return { ok: false, diagnostics: [invalidDisposition(label, "each rule requires exact id, consumers, and test fields")] };
|
|
104
|
+
}
|
|
105
|
+
if (ruleIds.has(rule.id)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate rule disposition: ${rule.id}`)] };
|
|
106
|
+
ruleIds.add(rule.id);
|
|
107
|
+
for (const consumer of rule.consumers) {
|
|
108
|
+
if (!exactObject(consumer, contract.fields.consumer) || !identifier.test(consumer.scope) || consumer.status !== contract.status || typeof consumer.evidence !== "string" || consumer.evidence.trim() !== consumer.evidence || consumer.evidence.length === 0) {
|
|
109
|
+
return { ok: false, diagnostics: [invalidDisposition(label, `invalid consumer disposition for ${rule.id}`)] };
|
|
110
|
+
}
|
|
111
|
+
const key = `${rule.id}\0consumer\0${consumer.scope}`;
|
|
112
|
+
if (entries.has(key)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate disposition for ${rule.id} consumer ${consumer.scope}`)] };
|
|
113
|
+
entries.set(key, { ruleId: rule.id, type: "consumer", target: consumer.scope, status: consumer.status, evidence: consumer.evidence });
|
|
114
|
+
}
|
|
115
|
+
if (rule.test.status !== contract.status || typeof rule.test.evidence !== "string" || rule.test.evidence.trim() !== rule.test.evidence || rule.test.evidence.length === 0) {
|
|
116
|
+
return { ok: false, diagnostics: [invalidDisposition(label, `invalid test disposition for ${rule.id}`)] };
|
|
117
|
+
}
|
|
118
|
+
const testKey = `${rule.id}\0test\0${rule.id}`;
|
|
119
|
+
if (entries.has(testKey)) return { ok: false, diagnostics: [invalidDisposition(label, `duplicate rule disposition: ${rule.id}`)] };
|
|
120
|
+
entries.set(testKey, { ruleId: rule.id, type: "test", target: rule.id, status: rule.test.status, evidence: rule.test.evidence });
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, diagnostics: [], entries };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function allUnits(diffResult) {
|
|
126
|
+
const units = new Map();
|
|
127
|
+
for (const compiled of [diffResult.before, diffResult.after]) {
|
|
128
|
+
for (const unit of compiled.ir.units) units.set(unit.id, unit);
|
|
129
|
+
}
|
|
130
|
+
return units;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function incomingGraph(diffResult) {
|
|
134
|
+
const incoming = new Map();
|
|
135
|
+
for (const compiled of [diffResult.before, diffResult.after]) {
|
|
136
|
+
for (const unit of compiled.ir.units) {
|
|
137
|
+
for (const relation of unit.relations) {
|
|
138
|
+
if (!IMPACT_RELATIONS.has(relation.type)) continue;
|
|
139
|
+
const sources = incoming.get(relation.target) ?? new Set();
|
|
140
|
+
sources.add(unit.id);
|
|
141
|
+
incoming.set(relation.target, sources);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return incoming;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function impactingChanges(diffResult) {
|
|
149
|
+
return diffResult.diff.changes.filter((change) => !change.pointer.startsWith("/metadata/"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function impactedIds(diffResult) {
|
|
153
|
+
const incoming = incomingGraph(diffResult);
|
|
154
|
+
const selected = new Set();
|
|
155
|
+
const pending = [...new Set(impactingChanges(diffResult).map((change) => change.unitId))].sort(compareText);
|
|
156
|
+
while (pending.length > 0) {
|
|
157
|
+
const id = pending.shift();
|
|
158
|
+
if (selected.has(id)) continue;
|
|
159
|
+
selected.add(id);
|
|
160
|
+
for (const dependent of [...(incoming.get(id) ?? [])].sort(compareText)) if (!selected.has(dependent)) pending.push(dependent);
|
|
161
|
+
}
|
|
162
|
+
return [...selected].sort(compareText);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function requiredObligations(diffResult, impacted) {
|
|
166
|
+
const before = new Map(diffResult.before.ir.units.map((unit) => [unit.id, unit]));
|
|
167
|
+
const after = new Map(diffResult.after.ir.units.map((unit) => [unit.id, unit]));
|
|
168
|
+
const obligations = [];
|
|
169
|
+
for (const id of impacted) {
|
|
170
|
+
const candidates = [before.get(id), after.get(id)].filter(Boolean);
|
|
171
|
+
if (!candidates.some((unit) => unit.kind === "rule" && unit.lifecycle === "active")) continue;
|
|
172
|
+
const scopes = new Set(candidates.flatMap((unit) => unit.semantic?.scope ?? []));
|
|
173
|
+
for (const scope of [...scopes].sort(compareText)) obligations.push({ ruleId: id, type: "consumer", target: scope });
|
|
174
|
+
obligations.push({ ruleId: id, type: "test", target: id });
|
|
175
|
+
}
|
|
176
|
+
return obligations;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function unitLocation(diffResult, id) {
|
|
180
|
+
for (const [compiled, inputPath] of [[diffResult.after, diffResult.afterPath], [diffResult.before, diffResult.beforePath]]) {
|
|
181
|
+
const index = compiled.ir.units.findIndex((unit) => unit.id === id);
|
|
182
|
+
if (index < 0) continue;
|
|
183
|
+
const mapped = compiled.sourceMap.fields[`/units/${index}/id`];
|
|
184
|
+
return { ...mapped, file: portablePath(mapped.file, await rootFor(inputPath)) };
|
|
185
|
+
}
|
|
186
|
+
return { file: ".", range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }, sourcePointer: "/id" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function impactAuthorityPaths(beforePath, afterPath, dispositionsPath, { maxBytes = null, dispositionLabel = null } = {}) {
|
|
190
|
+
const diffResult = await diffAuthorityPaths(beforePath, afterPath);
|
|
191
|
+
if (!diffResult.ok) return { ...diffResult, impact: null };
|
|
192
|
+
diffResult.beforePath = beforePath;
|
|
193
|
+
diffResult.afterPath = afterPath;
|
|
194
|
+
const parsed = await loadDispositions(dispositionsPath, dispositionLabel ?? dispositionsPath);
|
|
195
|
+
if (!parsed.ok) {
|
|
196
|
+
const budget = overBudget(diffResult, null, maxBytes);
|
|
197
|
+
if (budget.failure) return budget.failure;
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
diagnostics: parsed.diagnostics,
|
|
201
|
+
fileCount: diffResult.fileCount,
|
|
202
|
+
unitCount: 0,
|
|
203
|
+
payloadBytes: budget.payloadBytes,
|
|
204
|
+
diff: diffResult.diff,
|
|
205
|
+
impact: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const impacted = impactedIds(diffResult);
|
|
209
|
+
const units = allUnits(diffResult);
|
|
210
|
+
const required = requiredObligations(diffResult, impacted);
|
|
211
|
+
const requiredKeys = new Set(required.map((item) => `${item.ruleId}\0${item.type}\0${item.target}`));
|
|
212
|
+
const diagnostics = [];
|
|
213
|
+
for (const obligation of required) {
|
|
214
|
+
const key = `${obligation.ruleId}\0${obligation.type}\0${obligation.target}`;
|
|
215
|
+
if (parsed.entries.has(key)) continue;
|
|
216
|
+
const mapped = await unitLocation(diffResult, obligation.ruleId);
|
|
217
|
+
diagnostics.push(makeDiagnostic({
|
|
218
|
+
code: "AUTH_IMPACT_UNDISPOSED",
|
|
219
|
+
file: mapped.file,
|
|
220
|
+
range: mapped.range,
|
|
221
|
+
pointer: mapped.sourcePointer,
|
|
222
|
+
reason: `${obligation.type} obligation is not disposed for ${obligation.ruleId}: ${obligation.target}`,
|
|
223
|
+
repair: "add an addressed disposition with explicit evidence; do not infer completion",
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
for (const [key, disposition] of parsed.entries) {
|
|
227
|
+
if (requiredKeys.has(key)) continue;
|
|
228
|
+
diagnostics.push(invalidDisposition(dispositionLabel ?? dispositionsPath, `disposition does not match a required impact obligation: ${disposition.ruleId} ${disposition.type} ${disposition.target}`));
|
|
229
|
+
}
|
|
230
|
+
const disposed = required.map((obligation) => ({
|
|
231
|
+
...obligation,
|
|
232
|
+
disposition: parsed.entries.get(`${obligation.ruleId}\0${obligation.type}\0${obligation.target}`) ?? null,
|
|
233
|
+
}));
|
|
234
|
+
const impact = {
|
|
235
|
+
format: IMPACT_FORMAT,
|
|
236
|
+
changedUnits: [...new Set(impactingChanges(diffResult).map((change) => change.unitId))].sort(compareText),
|
|
237
|
+
impactedUnits: impacted.map((id) => ({ id, kind: units.get(id)?.kind ?? null })),
|
|
238
|
+
obligations: disposed,
|
|
239
|
+
complete: diagnostics.length === 0,
|
|
240
|
+
};
|
|
241
|
+
const budget = overBudget(diffResult, impact, maxBytes);
|
|
242
|
+
if (budget.failure) return budget.failure;
|
|
243
|
+
return {
|
|
244
|
+
ok: diagnostics.length === 0,
|
|
245
|
+
diagnostics: sortDiagnostics(diagnostics),
|
|
246
|
+
fileCount: diffResult.fileCount,
|
|
247
|
+
unitCount: impacted.length,
|
|
248
|
+
payloadBytes: budget.payloadBytes,
|
|
249
|
+
diff: diffResult.diff,
|
|
250
|
+
impact,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Shared lexical constants for the report products that read authority text.
|
|
2
|
+
//
|
|
3
|
+
// These lived as per-product copies until an audit found the copies had
|
|
4
|
+
// drifted apart: the embedded-path extractor built segments from one
|
|
5
|
+
// character set but tested the left boundary against a narrower one, and
|
|
6
|
+
// closed-sets scanned two of the four normative fields that anchors scanned.
|
|
7
|
+
// Both products now derive from this module so a change lands once.
|
|
8
|
+
|
|
9
|
+
// A path segment as the embedded-path (class C) grammar admits it. Anything
|
|
10
|
+
// in this set appearing immediately before a match means the match started
|
|
11
|
+
// mid-segment and is not a path of its own.
|
|
12
|
+
export const SEGMENT_CHARACTERS = "A-Za-z0-9_.-";
|
|
13
|
+
export const SEGMENT_PATTERN = `[${SEGMENT_CHARACTERS}]+`;
|
|
14
|
+
const SEGMENT_CHARACTER = new RegExp(`[${SEGMENT_CHARACTERS}]`, "u");
|
|
15
|
+
const PATH_SEPARATOR = new RegExp("[/\\\\]", "u");
|
|
16
|
+
|
|
17
|
+
// True when the match starting at `index` is not a path of its own, because
|
|
18
|
+
// what precedes it either continues a segment or makes it the tail of a longer
|
|
19
|
+
// path. A leading "./" or "../" is a relative prefix rather than a longer path,
|
|
20
|
+
// so it does not disqualify the match; a leading separator with no relative
|
|
21
|
+
// prefix reads as an absolute path, which the grammar does not admit.
|
|
22
|
+
export function continuesPathToken(text, index) {
|
|
23
|
+
if (index === 0) return false;
|
|
24
|
+
const preceding = text[index - 1];
|
|
25
|
+
if (SEGMENT_CHARACTER.test(preceding)) return true;
|
|
26
|
+
if (!PATH_SEPARATOR.test(preceding)) return false;
|
|
27
|
+
let start = index - 1;
|
|
28
|
+
while (start > 0 && SEGMENT_CHARACTER.test(text[start - 1])) start -= 1;
|
|
29
|
+
const run = text.slice(start, index - 1);
|
|
30
|
+
return run !== "." && run !== "..";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A candidate ending before another segment character or separator is only a
|
|
34
|
+
// prefix of the path token, so accepting it could turn a dead coordinate into
|
|
35
|
+
// an unrelated live one.
|
|
36
|
+
export function continuesPathTokenAfter(text, index) {
|
|
37
|
+
if (index >= text.length) return false;
|
|
38
|
+
return SEGMENT_CHARACTER.test(text[index]) || PATH_SEPARATOR.test(text[index]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const CODE_EXTENSIONS = [
|
|
42
|
+
"mjs", "js", "ts", "tsx", "rs", "go", "yaml", "yml", "md", "json", "proto", "ps1",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// The normative text of a unit: what the product actually asserts, excluding
|
|
46
|
+
// metadata such as title and reason. Every product that scans authority prose
|
|
47
|
+
// for machine-checkable content uses exactly this set.
|
|
48
|
+
export const DEFINITION_NORMATIVE_FIELDS = ["meaning"];
|
|
49
|
+
export const RULE_NORMATIVE_FIELDS = ["statement", "condition", "failure"];
|
|
50
|
+
|
|
51
|
+
export function normativeFields(unitKind) {
|
|
52
|
+
return unitKind === "definition" ? DEFINITION_NORMATIVE_FIELDS : RULE_NORMATIVE_FIELDS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const NORMATIVE_FIELD_HELP = "statement, condition, failure, and meaning";
|