@tikoci/centrs 0.1.3 → 0.1.6
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 +348 -0
- package/GLOSSARY.txt +252 -0
- package/LICENSE +21 -0
- package/README.md +44 -12
- package/commands/AGENTS.md +14 -5
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +56 -26
- package/commands/api/examples.md +46 -1
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +6 -1
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +77 -36
- package/commands/execute/examples.md +120 -10
- package/commands/explain/README.md +2756 -0
- package/commands/explain/examples.md +702 -0
- package/commands/mcp/README.md +7 -4
- package/commands/mcp/examples.md +2 -2
- package/commands/retrieve/README.md +25 -27
- package/commands/retrieve/examples.md +16 -0
- package/commands/terminal/examples.md +7 -1
- package/commands/transfer/README.md +28 -20
- package/commands/transfer/examples.md +29 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +138 -30
- package/docs/MATRIX.md +92 -18
- package/docs/errors/README.md +3 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/syntax.md +29 -4
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +56 -10
- package/src/api-fanout.ts +46 -18
- package/src/api.ts +351 -121
- package/src/cli/api.ts +26 -7
- package/src/cli/btest.ts +19 -3
- package/src/cli/execute.ts +25 -6
- package/src/cli/explain.ts +503 -0
- package/src/cli/retrieve.ts +33 -1
- package/src/cli/selection.ts +71 -11
- package/src/cli/settings.ts +2 -2
- package/src/cli/terminal.ts +1 -1
- package/src/cli/transfer.ts +23 -6
- package/src/cli.ts +15 -2
- package/src/core/envelope.ts +58 -2
- package/src/core/error-catalog.ts +42 -1
- package/src/core/execute-command.ts +148 -0
- package/src/core/inspect.ts +36 -4
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +47 -18
- package/src/execute.ts +339 -307
- package/src/explain/arg-tokens.ts +151 -0
- package/src/explain/args.ts +1749 -0
- package/src/explain/blocks.ts +175 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +39 -0
- package/src/explain/catalog.ts +1343 -0
- package/src/explain/coordinates.ts +488 -0
- package/src/explain/defects.ts +218 -0
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +668 -0
- package/src/explain/operator-tokens.ts +316 -0
- package/src/explain/operators.ts +465 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1569 -0
- package/src/explain/quoted-string.ts +331 -0
- package/src/explain/scope-brace.ts +461 -0
- package/src/explain/segment.ts +1165 -0
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +77 -0
- package/src/explain/symbol-values.ts +556 -0
- package/src/explain/symbols.ts +1617 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +514 -0
- package/src/explain/value-tokens.ts +83 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +955 -0
- package/src/explain/write.ts +856 -0
- package/src/explain.ts +2602 -0
- package/src/index.ts +225 -6
- package/src/mcp/server.ts +2 -1
- package/src/offline-gate.ts +148 -0
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/facts.ts +1 -1
- package/src/resolver/index.ts +23 -4
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +99 -12
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/target.ts +1 -1
- package/src/retrieve-fanout.ts +27 -13
- package/src/retrieve.ts +82 -77
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +43 -26
- package/src/transfer.ts +88 -45
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape fill — claims valid string-internal escape sequences on the residual.
|
|
3
|
+
*
|
|
4
|
+
* The one split #264 B5's rule admits and PR #330 did not make. B5's rule is
|
|
5
|
+
* that a merged class is split only where a consumer needs the distinction AND
|
|
6
|
+
* cannot recover it by joining `tokens[]` to another published surface on byte
|
|
7
|
+
* offsets. A valid escape passes both halves: only INVALID escapes surface
|
|
8
|
+
* elsewhere (as `bad-string-escape` diagnostics), so a consumer holding the
|
|
9
|
+
* token stream has no published surface to join against to find the valid ones,
|
|
10
|
+
* and the device does name them — `escaped` is one of the 19 classes the Q13
|
|
11
|
+
* capture observed.
|
|
12
|
+
*
|
|
13
|
+
* Unlike `arg-sep` this needs a real walk rather than a relabel of one
|
|
14
|
+
* analyzer's own output, and it takes bytes out of TWO fills at once rather
|
|
15
|
+
* than one. In byte terms it is still a retag: measured over the corpus
|
|
16
|
+
* `escaped` claims 19,960 bytes, `string` gives up 17,672 and `value` 2,288,
|
|
17
|
+
* and classified coverage is identical to the last decimal. Nothing that was
|
|
18
|
+
* `unclassified` becomes claimed — an escape only ever lives inside a run some
|
|
19
|
+
* fill already held.
|
|
20
|
+
*
|
|
21
|
+
* The walk is not this module's. `src/explain/quoted-string.ts` already owned
|
|
22
|
+
* the escape grammar for `bad-string-escape`, and `walkStringEscapes` now
|
|
23
|
+
* returns both halves of that single reading, so an `escaped` token and a
|
|
24
|
+
* `bad-string-escape` diagnostic cannot disagree about which bytes are an
|
|
25
|
+
* escape. This module only clips that list onto the residual.
|
|
26
|
+
*
|
|
27
|
+
* ## Fill order: before `value`, after `arg`
|
|
28
|
+
*
|
|
29
|
+
* An escape is interior to a quoted run, so it must claim before the two fills
|
|
30
|
+
* that would otherwise swallow it whole (`value`, then `string`) — after them
|
|
31
|
+
* it could claim nothing. It runs after `arg` because an argument NAME run is
|
|
32
|
+
* the analyzer's own located token and nothing inside it is a string interior;
|
|
33
|
+
* leaving `arg` ahead keeps the located reading winning over a byte scan, which
|
|
34
|
+
* is the same precedence every earlier fill was given (#290 design decision 1).
|
|
35
|
+
* Splitting a `value` or `string` run into fragments around an escape is the
|
|
36
|
+
* mechanism those fills already have — `~1,720` leaf value bytes were already
|
|
37
|
+
* being fragmented by `variable-*` spans before this fill existed.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type { ExplainToken } from "../explain.ts";
|
|
41
|
+
import type { StringEscapeSpan } from "./quoted-string.ts";
|
|
42
|
+
import { clipToResidual } from "./token-ranges.ts";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Valid string-internal escape spans on the residual.
|
|
46
|
+
*
|
|
47
|
+
* `analyzed` is the ASCII-normalized document text (length checks only);
|
|
48
|
+
* `residual` is the gap set left by earlier fills (sorted, no overlaps);
|
|
49
|
+
* `escapes` are the valid escapes from `walkStringEscapes`, already in
|
|
50
|
+
* analyzed-byte space. Every emitted span's bytes are fully inside `residual`,
|
|
51
|
+
* sorted by `start`, non-overlapping, and carry `class: "escaped"`.
|
|
52
|
+
*/
|
|
53
|
+
export function escapeSpans(
|
|
54
|
+
analyzed: string,
|
|
55
|
+
residual: readonly { start: number; end: number }[],
|
|
56
|
+
escapes: readonly StringEscapeSpan[],
|
|
57
|
+
): ExplainToken[] {
|
|
58
|
+
const len = analyzed.length;
|
|
59
|
+
if (len === 0 || residual.length === 0 || escapes.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
const out: ExplainToken[] = [];
|
|
62
|
+
for (const { start, end } of escapes) {
|
|
63
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
|
|
64
|
+
if (start < 0 || end <= start || end > len) continue;
|
|
65
|
+
for (const r of clipToResidual(start, end, residual)) {
|
|
66
|
+
out.push({
|
|
67
|
+
start: r.start,
|
|
68
|
+
end: r.end,
|
|
69
|
+
class: "escaped" as const,
|
|
70
|
+
ev: "e16",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
out.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `centrs → highlight` projection (#264 B4, measured by #263).
|
|
3
|
+
*
|
|
4
|
+
* `data.tokens[]` is a centrs-owned vocabulary; RouterOS
|
|
5
|
+
* `/console/inspect request=highlight` is a *different* vocabulary produced by
|
|
6
|
+
* a *different* kind of analyzer. This module is the declared bridge between
|
|
7
|
+
* them, and it exists so the agreement report
|
|
8
|
+
* (`scripts/explain-highlight-agreement.ts`) can say what offline analysis got
|
|
9
|
+
* right against device truth instead of eyeballing two streams side by side.
|
|
10
|
+
*
|
|
11
|
+
* ## The projection is declared, never fitted
|
|
12
|
+
*
|
|
13
|
+
* Every entry below is justified by what the centrs class was *defined* to
|
|
14
|
+
* mean — `commands/explain/README.md` → the token-census block and
|
|
15
|
+
* `src/explain.ts`'s `ExplainTokenClass` doc — not by which device class it
|
|
16
|
+
* happened to land on most often. A projection tuned to the measurement would
|
|
17
|
+
* make the measurement circular: agreement would only ever say "the map was
|
|
18
|
+
* fitted", which is not a fact about the parser. So a centrs class whose
|
|
19
|
+
* *intent* has no single device counterpart projects to `null` and the report
|
|
20
|
+
* counts those bytes as `unprojected`, reporting what the device said about
|
|
21
|
+
* them as its own table. That table is the evidence #264 read to decide
|
|
22
|
+
* which provisional merges deserved to be split — two did, `arg-sep` (B5) and
|
|
23
|
+
* `escaped` — and `commands/explain/README.md` -> *The vocabulary, and what
|
|
24
|
+
* earns a class* records why the rest stay merged.
|
|
25
|
+
*
|
|
26
|
+
* ## Why the device stream is not one oracle but four
|
|
27
|
+
*
|
|
28
|
+
* Scoring a byte requires knowing what kind of answer the device gave, and
|
|
29
|
+
* `highlight` mixes four:
|
|
30
|
+
*
|
|
31
|
+
* - **syntax** — a class an offline analyzer can in principle decide from the
|
|
32
|
+
* source text alone (`cmd`, `dir`, `arg`, `comment`, the `variable-*`
|
|
33
|
+
* family, `escaped`, `syntax-meta`).
|
|
34
|
+
* - **non-syntax** — `obj-inactive`, `obj-dynamic`, `obj-disabled`,
|
|
35
|
+
* `variable-undefined`, `syntax-obsolete`. The source text alone does not
|
|
36
|
+
* decide these, so they are reported as their own bucket and never as a
|
|
37
|
+
* wrong answer. Note what abstains: centrs has no class for most of them
|
|
38
|
+
* (`ExplainSymbolClass` excludes `undefined`), but where it *does* claim the
|
|
39
|
+
* byte — an `arg` the device calls `obj-inactive` — the token stands and it
|
|
40
|
+
* is the SCORER that declines to grade it. Saying *why* each one is out of
|
|
41
|
+
* reach is a separate question, and it is deliberately **not** answered by
|
|
42
|
+
* the class name — see `applicabilityOf` below.
|
|
43
|
+
* - **silence** — `none`. The device assigned no class. It is the device's
|
|
44
|
+
* abstention, not a class named "none", so agreeing with it is not an
|
|
45
|
+
* agreement (probes: "an abstention is not a disagreement").
|
|
46
|
+
* - **parser-stop** — `error`. Measured over the committed slice this byte
|
|
47
|
+
* appears **at most once per script, always exactly one byte wide**, and on
|
|
48
|
+
* long-term 7.23.5 every byte after it is `none`: the device stops
|
|
49
|
+
* classifying rather than recovering. Most of the slice's bytes sit in that
|
|
50
|
+
* tail, so a percentage that ignores it measures the device giving up.
|
|
51
|
+
* Stable 7.24.2 and development 7.25beta3 do classify again after the `error`
|
|
52
|
+
* in one run, which is why the report gives that a bucket of its own instead
|
|
53
|
+
* of asserting the rule (the counts live in the generated README block, not
|
|
54
|
+
* here, so they cannot go stale). The rule and its exception swapped builds
|
|
55
|
+
* with the #342 channel realignment; the shape of the claim did not.
|
|
56
|
+
*
|
|
57
|
+
* ## Upstream drift
|
|
58
|
+
*
|
|
59
|
+
* `DEVICE_CLASS_KIND` lists the 19 classes the phase-0 capture observed on
|
|
60
|
+
* 7.23.2 / 7.24rc2 — and the #342 recapture found the SAME 19 on 7.24.2,
|
|
61
|
+
* 7.23.5 and 7.25beta3, so the vocabulary has held across five builds and two
|
|
62
|
+
* capture rounds. A class outside it is `"unknown"` — reported, never
|
|
63
|
+
* silently folded into one of the four kinds — because a new upstream class is
|
|
64
|
+
* exactly the drift the centrs-owned vocabulary exists to absorb, and guessing
|
|
65
|
+
* its kind would hide it.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import type { ExplainTokenClass } from "../explain.ts";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What kind of answer a device highlight class is, from an offline analyzer's
|
|
72
|
+
* point of view. See the module header for why the distinction is load-bearing.
|
|
73
|
+
*/
|
|
74
|
+
export type DeviceClassKind =
|
|
75
|
+
| "syntax"
|
|
76
|
+
| "non-syntax"
|
|
77
|
+
| "silence"
|
|
78
|
+
| "parser-stop"
|
|
79
|
+
| "unknown";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The 19 classes the Q13 capture observed, by kind.
|
|
83
|
+
*
|
|
84
|
+
* Source: `test/fixtures/explain/highlight-streams.slice.json` →
|
|
85
|
+
* `selection.classesInCapture`, which the slice's own integrity test pins
|
|
86
|
+
* against the stream data. Keep the two in step: the agreement report fails
|
|
87
|
+
* loudly when the slice carries a class this table does not name.
|
|
88
|
+
*/
|
|
89
|
+
export const DEVICE_CLASS_KIND: Readonly<Record<string, DeviceClassKind>> = {
|
|
90
|
+
arg: "syntax",
|
|
91
|
+
"arg-dot": "syntax",
|
|
92
|
+
"arg-scope": "syntax",
|
|
93
|
+
cmd: "syntax",
|
|
94
|
+
comment: "syntax",
|
|
95
|
+
dir: "syntax",
|
|
96
|
+
escaped: "syntax",
|
|
97
|
+
"syntax-meta": "syntax",
|
|
98
|
+
"variable-auto": "syntax",
|
|
99
|
+
"variable-global": "syntax",
|
|
100
|
+
"variable-local": "syntax",
|
|
101
|
+
"variable-parameter": "syntax",
|
|
102
|
+
"obj-disabled": "non-syntax",
|
|
103
|
+
"obj-dynamic": "non-syntax",
|
|
104
|
+
"obj-inactive": "non-syntax",
|
|
105
|
+
"syntax-obsolete": "non-syntax",
|
|
106
|
+
"variable-undefined": "non-syntax",
|
|
107
|
+
none: "silence",
|
|
108
|
+
error: "parser-stop",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The kind of a device class; `"unknown"` for anything the capture never saw. */
|
|
112
|
+
export function deviceClassKind(deviceClass: string): DeviceClassKind {
|
|
113
|
+
return DEVICE_CLASS_KIND[deviceClass] ?? "unknown";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* One projection entry: the device classes a centrs class predicts, plus the
|
|
118
|
+
* reason it predicts them. `accepts: null` is a deliberate abstention.
|
|
119
|
+
*/
|
|
120
|
+
export interface ProjectionEntry {
|
|
121
|
+
/** Device classes this centrs class claims, or `null` to abstain. */
|
|
122
|
+
readonly accepts: readonly string[] | null;
|
|
123
|
+
/** Why — in terms of what the centrs class was defined to mean. */
|
|
124
|
+
readonly because: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* centrs token class → the device highlight classes it predicts.
|
|
129
|
+
*
|
|
130
|
+
* Total over `ExplainTokenClass`: every class has an entry, and an entry is
|
|
131
|
+
* either a non-empty accept set or an explicit abstention.
|
|
132
|
+
*/
|
|
133
|
+
export const HIGHLIGHT_PROJECTION: Readonly<
|
|
134
|
+
Record<ExplainTokenClass, ProjectionEntry>
|
|
135
|
+
> = {
|
|
136
|
+
comment: {
|
|
137
|
+
accepts: ["comment"],
|
|
138
|
+
because:
|
|
139
|
+
"name-coincident: the class was taken from the device vocabulary and means the same run of source",
|
|
140
|
+
},
|
|
141
|
+
"variable-local": {
|
|
142
|
+
accepts: ["variable-local"],
|
|
143
|
+
because: "name-coincident (the Q13 symbol classes are the device's own)",
|
|
144
|
+
},
|
|
145
|
+
"variable-global": {
|
|
146
|
+
accepts: ["variable-global"],
|
|
147
|
+
because: "name-coincident (the Q13 symbol classes are the device's own)",
|
|
148
|
+
},
|
|
149
|
+
"variable-auto": {
|
|
150
|
+
accepts: ["variable-auto"],
|
|
151
|
+
because: "name-coincident (the Q13 symbol classes are the device's own)",
|
|
152
|
+
},
|
|
153
|
+
"variable-parameter": {
|
|
154
|
+
accepts: ["variable-parameter"],
|
|
155
|
+
because: "name-coincident (the Q13 symbol classes are the device's own)",
|
|
156
|
+
},
|
|
157
|
+
dir: {
|
|
158
|
+
accepts: ["dir"],
|
|
159
|
+
because:
|
|
160
|
+
"name-coincident: resolved menu bytes, leading `/` or `:` included, which is the run the device also calls `dir`",
|
|
161
|
+
},
|
|
162
|
+
cmd: {
|
|
163
|
+
accepts: ["cmd"],
|
|
164
|
+
because: "name-coincident: the resolved command-name run",
|
|
165
|
+
},
|
|
166
|
+
arg: {
|
|
167
|
+
accepts: ["arg", "arg-dot", "arg-scope"],
|
|
168
|
+
because:
|
|
169
|
+
"argument-name bytes; the device splits dotted spellings (`.id`, `security.authentication-types`) into `arg-dot` / `arg-scope` and centrs deliberately does not, so all three are the same claim. The `=` is no longer part of this class — #264 B5 gave it `arg-sep`, which is why this entry now predicts only name classes",
|
|
170
|
+
},
|
|
171
|
+
"arg-sep": {
|
|
172
|
+
accepts: ["syntax-meta"],
|
|
173
|
+
because:
|
|
174
|
+
"the `=` binding one argument name to its value is structure punctuation, not part of the name, and `syntax-meta` is the device's only class for structure. Declared from what the class means, not fitted: before #264 B5 split it out these bytes were the whole of the `arg` -> `syntax-meta` disagreement, which is the measurement that made the split worth its name (the count is in the generated agreement block, so it cannot go stale here)",
|
|
175
|
+
},
|
|
176
|
+
operator: {
|
|
177
|
+
accepts: ["syntax-meta"],
|
|
178
|
+
because:
|
|
179
|
+
"`syntax-meta` is the device's residual structure class and the only place an operator byte can land (#255): the device merges adjacent structure runs, so it names no operator boundary, but it does decide structure-versus-word",
|
|
180
|
+
},
|
|
181
|
+
brace: {
|
|
182
|
+
accepts: ["syntax-meta"],
|
|
183
|
+
because:
|
|
184
|
+
"scope and array delimiters are structure punctuation, and `syntax-meta` is the device's only class for it",
|
|
185
|
+
},
|
|
186
|
+
string: {
|
|
187
|
+
accepts: null,
|
|
188
|
+
because:
|
|
189
|
+
"the token spans the quoted run MINUS its escapes, which the device still splits two ways — delimiters `syntax-meta` and the interior it declines to classify at all. Naming the escapes (#264) removed the third way and is why this entry's residue is now delimiters, not delimiters plus escapes; the run still has no single device class, so the abstention stands",
|
|
190
|
+
},
|
|
191
|
+
escaped: {
|
|
192
|
+
accepts: ["escaped"],
|
|
193
|
+
because:
|
|
194
|
+
"name-coincident: the class means one valid string-internal escape sequence, which is the run the device also calls `escaped`. Declared from meaning before it was measured — `escaped` was already in `DEVICE_CLASS_KIND` as a syntax class, and the agreement report's `unprojected` table is what showed the bytes were being merged away rather than what chose the target",
|
|
195
|
+
},
|
|
196
|
+
value: {
|
|
197
|
+
accepts: null,
|
|
198
|
+
because:
|
|
199
|
+
"the device has no value class. It classifies an argument's value only when the value is itself something else (a substitution, an object name, an escape — the last of which #264 split out), so a `value` token has no device counterpart to be right or wrong about",
|
|
200
|
+
},
|
|
201
|
+
unclassified: {
|
|
202
|
+
accepts: null,
|
|
203
|
+
because: "centrs's own abstention — no analyzer claimed the byte",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** The device classes a centrs class predicts, or `null` where it abstains. */
|
|
208
|
+
export function projectTokenClass(
|
|
209
|
+
tokenClass: ExplainTokenClass,
|
|
210
|
+
): readonly string[] | null {
|
|
211
|
+
return HIGHLIGHT_PROJECTION[tokenClass].accepts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Why a device answer is or is not within reach of offline analysis (#263).
|
|
216
|
+
*
|
|
217
|
+
* This is a **second axis**, orthogonal to whether a byte agreed. Agreement
|
|
218
|
+
* asks "did the two answers match"; applicability asks "what kind of fact was
|
|
219
|
+
* the device deciding, and could source text alone have decided it". A byte
|
|
220
|
+
* has one of each.
|
|
221
|
+
*
|
|
222
|
+
* The categories are #263's, and the load-bearing instruction there is that
|
|
223
|
+
* they are assigned **from context, never by assigning a whole highlight class
|
|
224
|
+
* to one bucket**. So the input is the *pair* — what centrs read the byte as
|
|
225
|
+
* and what the device called it — plus whether the captured versions even
|
|
226
|
+
* agreed on the device's answer.
|
|
227
|
+
*/
|
|
228
|
+
export type Applicability =
|
|
229
|
+
/** Syntax and structure justify the conclusion; offline analysis can reach it. */
|
|
230
|
+
| "offline-decidable"
|
|
231
|
+
/** Needs a per-path field/argument schema offline centrs deliberately does not ship. */
|
|
232
|
+
| "schema-dependent"
|
|
233
|
+
/** Needs runtime objects or session state outside the input. */
|
|
234
|
+
| "state-dependent"
|
|
235
|
+
/**
|
|
236
|
+
* Deciding needs version context: either the captured RouterOS versions
|
|
237
|
+
* disagree about this byte, so there is no single device answer, or the
|
|
238
|
+
* device's answer is itself a statement about the running version
|
|
239
|
+
* (`syntax-obsolete`) even where the captures agree.
|
|
240
|
+
*/
|
|
241
|
+
| "version-dependent"
|
|
242
|
+
/** No supported basis for a category. Explicit, never a silent default. */
|
|
243
|
+
| "uncategorized"
|
|
244
|
+
/** The device gave no answer here (`none`, or the post-`error` tail). */
|
|
245
|
+
| "no-device-answer";
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Categorize one byte's device answer.
|
|
249
|
+
*
|
|
250
|
+
* `versionsAgree` is whether every captured version gave this byte the same
|
|
251
|
+
* class. It is checked **first**, ahead even of the device's own silence: when
|
|
252
|
+
* the captures disagree there is no single device answer to be schema- or
|
|
253
|
+
* state-dependent about, and the disagreement is itself the fact worth
|
|
254
|
+
* reporting. That ordering matters in practice — a byte one build classifies
|
|
255
|
+
* and the next leaves `none` is a version difference, not an absence, and
|
|
256
|
+
* checking silence first would file it as "the device said nothing" on the
|
|
257
|
+
* strength of whichever build happens to be the base.
|
|
258
|
+
*/
|
|
259
|
+
export function applicabilityOf(
|
|
260
|
+
centrsClass: string,
|
|
261
|
+
deviceClass: string,
|
|
262
|
+
versionsAgree: boolean,
|
|
263
|
+
): Applicability {
|
|
264
|
+
if (!versionsAgree) return "version-dependent";
|
|
265
|
+
const kind = deviceClassKind(deviceClass);
|
|
266
|
+
if (kind === "silence" || kind === "parser-stop") return "no-device-answer";
|
|
267
|
+
if (kind === "syntax") return "offline-decidable";
|
|
268
|
+
// `syntax-obsolete` is the running version's deprecation table talking: the
|
|
269
|
+
// same bytes are unremarkable on an older build. Measured on the slice it
|
|
270
|
+
// lands on the whitespace where modern syntax wants `=` (`} else {`,
|
|
271
|
+
// `:foreach conn in $conns`).
|
|
272
|
+
if (deviceClass === "syntax-obsolete") return "version-dependent";
|
|
273
|
+
if (deviceClass === "obj-dynamic" || deviceClass === "obj-disabled")
|
|
274
|
+
return "state-dependent";
|
|
275
|
+
if (deviceClass === "obj-inactive") {
|
|
276
|
+
// Same class, two different reasons — which is exactly why the class name
|
|
277
|
+
// cannot decide the category. On a name centrs read as an argument, the
|
|
278
|
+
// device is saying the menu's schema has no such field; on a menu or
|
|
279
|
+
// command name it is saying the package or menu is not present on this
|
|
280
|
+
// box.
|
|
281
|
+
//
|
|
282
|
+
// `arg-sep` rides with `arg` because it is the same fact about the same
|
|
283
|
+
// attribute: the device marking an attribute inactive marks the `=` that
|
|
284
|
+
// binds it too, and what it is saying there is still "this menu has no
|
|
285
|
+
// such field". Splitting the class (#264 B5) did not split the reason.
|
|
286
|
+
if (centrsClass === "arg" || centrsClass === "arg-sep")
|
|
287
|
+
return "schema-dependent";
|
|
288
|
+
if (centrsClass === "dir" || centrsClass === "cmd")
|
|
289
|
+
return "state-dependent";
|
|
290
|
+
return "uncategorized";
|
|
291
|
+
}
|
|
292
|
+
// `variable-undefined` is NOT blanket state-dependent (#263, explicitly). A
|
|
293
|
+
// bare word the device resolved to an undefined reference may be an external
|
|
294
|
+
// global, a query/filter field name, or a lexical spelling — three different
|
|
295
|
+
// categories — and nothing committed here separates them. Equally, an absent
|
|
296
|
+
// binding in this document is not evidence of a runtime undefined variable.
|
|
297
|
+
// Leaving it explicit is the point; folding it into a category would be the
|
|
298
|
+
// unsupported claim.
|
|
299
|
+
//
|
|
300
|
+
// This branch reaches the same answer as the fallthrough below on purpose:
|
|
301
|
+
// the requirement belongs at the point of decision, where the next reader
|
|
302
|
+
// making a rule for this class will be standing, not only in the prose. It
|
|
303
|
+
// is pinned by a test.
|
|
304
|
+
if (deviceClass === "variable-undefined") return "uncategorized";
|
|
305
|
+
// Any other non-syntax class, and any class this build has never seen: no
|
|
306
|
+
// supported basis for a category, said out loud rather than guessed.
|
|
307
|
+
return "uncategorized";
|
|
308
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { lookupPath } from "./catalog.ts";
|
|
2
|
+
import { isMenuPath } from "./menus.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether `segments` name a known RouterOS menu or settings path.
|
|
6
|
+
*
|
|
7
|
+
* Union of the two shipped structure tables (#235): `MENU_PATHS` (#207,
|
|
8
|
+
* `isMenuPath`) and `PATH_CATALOG` menu/settings rows (#228, `lookupPath`).
|
|
9
|
+
* Presence is load-bearing, absence abstains — the caller must not read
|
|
10
|
+
* absence as evidence of a command.
|
|
11
|
+
*/
|
|
12
|
+
export function isKnownMenuPath(segments: readonly string[]): boolean {
|
|
13
|
+
const entry = lookupPath(segments);
|
|
14
|
+
return (
|
|
15
|
+
isMenuPath(segments) || entry?.kind === "menu" || entry?.kind === "settings"
|
|
16
|
+
);
|
|
17
|
+
}
|