@tikoci/centrs 0.1.4 → 0.1.7
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 +417 -1
- package/GLOSSARY.txt +40 -1
- package/README.md +16 -8
- package/commands/AGENTS.md +7 -4
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +50 -10
- package/commands/api/examples.md +100 -2
- package/commands/devices/README.md +10 -1
- package/commands/devices/examples.md +33 -2
- package/commands/execute/README.md +68 -25
- package/commands/execute/examples.md +168 -7
- package/commands/explain/README.md +1206 -155
- package/commands/explain/examples.md +99 -3
- package/commands/mcp/README.md +9 -5
- package/commands/mcp/examples.md +2 -2
- package/commands/retrieve/examples.md +20 -0
- package/commands/terminal/examples.md +7 -1
- package/docs/CLI.md +3 -3
- package/docs/CONSTITUTION.md +83 -21
- package/docs/MATRIX.md +89 -52
- package/docs/errors/cdb/create-failed.md +20 -0
- package/docs/errors/cdb/file-exists.md +23 -0
- package/docs/errors/input/invalid-target.md +25 -3
- package/docs/errors/routeros/invalid-value.md +3 -2
- package/docs/errors/transport/cancel-unacknowledged.md +24 -0
- package/docs/errors/validation/syntax.md +29 -4
- package/package.json +13 -2
- package/src/api-fanout.ts +17 -4
- package/src/api.ts +346 -75
- package/src/cli/api.ts +1 -1
- package/src/cli/btest.ts +1 -1
- package/src/cli/devices.ts +16 -3
- package/src/cli/explain.ts +1 -1
- package/src/cli/terminal.ts +1 -1
- package/src/core/envelope.ts +53 -0
- package/src/core/error-catalog.ts +16 -1
- package/src/core/execute-command.ts +455 -0
- package/src/core/inspect.ts +58 -26
- package/src/core/routeros-errors.ts +66 -0
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/data/winbox-cdb-write.ts +22 -8
- package/src/devices.ts +142 -28
- package/src/execute-fanout.ts +19 -5
- package/src/execute.ts +247 -295
- package/src/explain/arg-tokens.ts +62 -25
- package/src/explain/args.ts +398 -52
- package/src/explain/blocks.ts +54 -11
- package/src/explain/brace-tokens.ts +4 -3
- package/src/explain/catalog.ts +42 -44
- package/src/explain/coordinates.ts +159 -0
- package/src/explain/defects.ts +2 -1
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/menus.ts +9 -5
- package/src/explain/operator-tokens.ts +13 -7
- package/src/explain/operators.ts +27 -20
- package/src/explain/path-tokens.ts +1 -1
- package/src/explain/pathresolve.ts +358 -113
- package/src/explain/quoted-string.ts +110 -20
- package/src/explain/scope-brace.ts +175 -22
- package/src/explain/segment.ts +429 -21
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +14 -4
- package/src/explain/symbol-values.ts +209 -173
- package/src/explain/symbols.ts +21 -1
- package/src/explain/transport.ts +17 -0
- package/src/explain/value-tokens.ts +15 -12
- package/src/explain/verbsplit.ts +165 -23
- package/src/explain/write.ts +47 -6
- package/src/explain.ts +522 -72
- package/src/index.ts +21 -2
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1 -1
- package/src/offline-gate.ts +148 -0
- package/src/protocols/adapter.ts +12 -1
- package/src/protocols/mac-telnet-console.ts +6 -4
- package/src/protocols/native-api.ts +29 -3
- package/src/resolver/target.ts +32 -12
- package/src/retrieve-fanout.ts +1 -1
- package/src/retrieve.ts +18 -63
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +1 -1
- package/src/transfer.ts +1 -1
package/src/explain/defects.ts
CHANGED
|
@@ -101,6 +101,7 @@ export type DefectCode =
|
|
|
101
101
|
| "bad-string-escape"
|
|
102
102
|
| "bad-sigil"
|
|
103
103
|
| "invalid-hash"
|
|
104
|
+
| "invalid-apostrophe"
|
|
104
105
|
| "unterminated-string"
|
|
105
106
|
| "unclosed"
|
|
106
107
|
| "unbalanced-close"
|
|
@@ -135,7 +136,7 @@ export function isPositionalFact(code: DefectCode): boolean {
|
|
|
135
136
|
* Does this list carry a defect an analyzer must ABSTAIN on?
|
|
136
137
|
*
|
|
137
138
|
* The abstention gate, and the reason {@link isPositionalFact} is a function
|
|
138
|
-
* rather than a comment: a defect list is not uniformly "bad input".
|
|
139
|
+
* rather than a comment: a defect list is not uniformly "bad input". Nine codes
|
|
139
140
|
* mark structure the analyzer could not resolve — part of the document was never
|
|
140
141
|
* walked, so no verdict over it is safe. The other two are coordinate facts about
|
|
141
142
|
* bytes that were read perfectly well. Gating on `defects.length > 0` would
|
|
@@ -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
|
+
}
|
package/src/explain/menus.ts
CHANGED
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
* the drift gate. The generator (`scripts/gen-explain-menus.ts`) carries the
|
|
6
6
|
* full rationale, the source pin, and why absence here is safe.
|
|
7
7
|
*
|
|
8
|
-
* Union of the `dir` and `path` nodes in
|
|
8
|
+
* Union of the `dir` and `path` nodes in 6 pinned restraml typed trees
|
|
9
9
|
* (`https://tikoci.github.io/restraml/`), all extra-packages builds:
|
|
10
10
|
*
|
|
11
11
|
* | Tree | Arch | RouterOS | Nodes | Containers |
|
|
12
12
|
* | ---- | ---- | -------- | ----- | ---------- |
|
|
13
13
|
* | `7.10.2/extra/inspect.json` | x86 | 7.10.2 | 29,086 | 458 |
|
|
14
14
|
* | `7.16/extra/inspect.json` | x86 | 7.16 | 34,876 | 522 |
|
|
15
|
-
* | `7.
|
|
16
|
-
* | `7.
|
|
15
|
+
* | `7.24.4/extra/deep-inspect.x86.json` | x86 | 7.24.4 | 41,345 | 554 |
|
|
16
|
+
* | `7.24.4/extra/deep-inspect.arm64.json` | arm64 | 7.24.4 | 42,691 | 575 |
|
|
17
|
+
* | `7.25beta5/extra/deep-inspect.x86.json` | x86 | 7.25beta5 | 41,794 | 557 |
|
|
18
|
+
* | `7.25beta5/extra/deep-inspect.arm64.json` | arm64 | 7.25beta5 | 43,150 | 578 |
|
|
17
19
|
*
|
|
18
20
|
* Zero node-type conflicts across those trees — no `dir`↔`cmd` flip across
|
|
19
|
-
*
|
|
21
|
+
* 4 versions or across architectures — which is what makes the union
|
|
20
22
|
* version-less rather than version-keyed. Generation aborts if that ever stops
|
|
21
23
|
* holding.
|
|
22
24
|
*
|
|
@@ -125,6 +127,7 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
|
|
|
125
127
|
"/interface/eoip",
|
|
126
128
|
"/interface/eoipv6",
|
|
127
129
|
"/interface/ethernet",
|
|
130
|
+
"/interface/ethernet/pon",
|
|
128
131
|
"/interface/ethernet/switch",
|
|
129
132
|
"/interface/ethernet/switch/host",
|
|
130
133
|
"/interface/ethernet/switch/port",
|
|
@@ -226,6 +229,7 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
|
|
|
226
229
|
"/interface/wireless/sniffer/packet",
|
|
227
230
|
"/interface/wireless/snooper",
|
|
228
231
|
"/interface/wireless/wds",
|
|
232
|
+
"/interface/xfrm",
|
|
229
233
|
"/iot",
|
|
230
234
|
"/iot/bluetooth",
|
|
231
235
|
"/iot/bluetooth/advertisers",
|
|
@@ -309,7 +313,6 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
|
|
|
309
313
|
"/ip/ipsec/installed-sa",
|
|
310
314
|
"/ip/ipsec/key",
|
|
311
315
|
"/ip/ipsec/key/psk",
|
|
312
|
-
"/ip/ipsec/key/qkd",
|
|
313
316
|
"/ip/ipsec/key/rsa",
|
|
314
317
|
"/ip/ipsec/mode-config",
|
|
315
318
|
"/ip/ipsec/peer",
|
|
@@ -354,6 +357,7 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
|
|
|
354
357
|
"/ip/socks/users",
|
|
355
358
|
"/ip/socksify",
|
|
356
359
|
"/ip/ssh",
|
|
360
|
+
"/ip/ssh/known-hosts",
|
|
357
361
|
"/ip/tftp",
|
|
358
362
|
"/ip/tftp/settings",
|
|
359
363
|
"/ip/traffic-flow",
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
* `(evl /system/identity/get value-name=name)` — the `/` are path
|
|
27
27
|
* separators and the `=` is an argument separator, with no division and no
|
|
28
28
|
* comparison node. So the `, / = -` conservatism holds everywhere except
|
|
29
|
-
* directly inside `(`. Measured cost: `find
|
|
30
|
-
*
|
|
31
|
-
* `unclassified
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* directly inside `(`. Measured cost: `find address=$IP` inside `[…]` *does*
|
|
30
|
+
* lower to real `(= $address $IP)` nodes, and those `=` bytes stay
|
|
31
|
+
* `unclassified`. The figures are generated, not asserted here — see
|
|
32
|
+
* `scripts/explain-bracket-equals.ts` and the gated block in
|
|
33
|
+
* `commands/explain/README.md`. The split runs roughly 40/60 query against
|
|
34
|
+
* `arg=value`, so abstaining beats claiming; a `where`-aware fill can take
|
|
35
|
+
* the query half later. The numbers this comment used to carry (224 of
|
|
36
|
+
* 1,259 against 1,035) were unreproducible and ungated, which is what #341
|
|
37
|
+
* fixed — do not re-inline a figure here.
|
|
34
38
|
* 2. **Glued after `=` is an argument value.** `in-interface-list=!LAN`,
|
|
35
39
|
* `.id=*2`, `oid=.1.3.6.1.2.1` — the byte after an argument `=` starts the
|
|
36
40
|
* value, never an operator.
|
|
@@ -40,8 +44,10 @@
|
|
|
40
44
|
* `security.authentication-types=wpa2-psk` as one name and renders `.id` as
|
|
41
45
|
* the single symbol `$.id`, never a `(. …)` concat.
|
|
42
46
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
47
|
+
* One `operator` class for all 26 spellings + the two aliases (`&&`, `||`).
|
|
48
|
+
* #264 B5 kept that merge: a consumer that wants the spelling slices the input
|
|
49
|
+
* at the token's span, and `src/explain/operators.ts` maps it to its IL node,
|
|
50
|
+
* so a per-operator legend would restate what the bytes already say.
|
|
45
51
|
* `<>` is **not** one token — it re-lexes to `<` then `>` as two tokens.
|
|
46
52
|
* `syntax-meta` is residual and merged, never a source (#255).
|
|
47
53
|
*/
|
package/src/explain/operators.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The RouterOS operator surface, as the DEVICE describes it (#255).
|
|
3
3
|
*
|
|
4
|
-
* Every row here was scored
|
|
5
|
-
*
|
|
6
|
-
* build
|
|
4
|
+
* Every row here was scored by `bun run explain:probe:operators` on the three
|
|
5
|
+
* builds the pinned corpus classes `complete` — stable 7.24.2 (which is also
|
|
6
|
+
* the current testing build), long-term 7.23.5 and development 7.25beta3 — so
|
|
7
|
+
* the operator axis and the token partition are measured on the same firmware
|
|
8
|
+
* (#342). A fourth capture, 7.21.5, is kept as an **age anchor** rather than a
|
|
9
|
+
* channel claim: it is the only live evidence that this surface behaves the
|
|
10
|
+
* same on a pre-7.23 branch. The reviewed slice is
|
|
7
11
|
* `test/fixtures/explain/operators.json` → `sweep`, and
|
|
8
12
|
* `test/unit/explain-operators.test.ts` asserts this table still equals it.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
13
|
+
* All four builds differ in exactly one row, and it is not a lexer row — see
|
|
14
|
+
* {@link ARRAY_COMPARISON_NOTE}.
|
|
11
15
|
*
|
|
12
16
|
* ## Why this is not the manual's list
|
|
13
17
|
*
|
|
@@ -176,7 +180,7 @@ const OPERATORS: readonly RouterosOperator[] = [
|
|
|
176
180
|
// them separately: every `(U 1 B 2)` and `(1 B U 2)` for `U` in
|
|
177
181
|
// `!`/`any`/`~`/`-`/`>` against all 24 binaries, 240 probes, published as
|
|
178
182
|
// the fixture's `unary` block. All 240 accept and `B` is outermost in
|
|
179
|
-
// every one, on
|
|
183
|
+
// every one, on all four swept builds alike — so both bind TIGHTER
|
|
180
184
|
// than every binary, including `->` (14) and `<%%` (13). That is level 15,
|
|
181
185
|
// right-assoc, shared with the unary arities of `~`, `-`, `>` (whose binary
|
|
182
186
|
// arities sit at 5, 11, 5). The single `precedence` per spelling is
|
|
@@ -362,24 +366,27 @@ function notOp(
|
|
|
362
366
|
* The single version-dependent fact the sweep found.
|
|
363
367
|
*
|
|
364
368
|
* Every operator, arity, precedence level, associativity, `highlight` class and
|
|
365
|
-
* op-axis row is identical on 7.
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* `sweep.versionDifferences` rather than in
|
|
369
|
+
* op-axis row is identical on all four swept builds — 7.24.2, 7.23.5,
|
|
370
|
+
* 7.25beta3 and the 7.21.5 age anchor — and identical is a measurement here,
|
|
371
|
+
* not a reading: `buildSweep` diffs each of those axes per version, so a build
|
|
372
|
+
* that re-ranked `->` would appear in `sweep.versionDifferences` rather than in
|
|
373
|
+
* nobody's notes.
|
|
369
374
|
*
|
|
370
375
|
* The one row that moved is a RUNTIME semantic, not a lexer fact:
|
|
371
|
-
* `:put ({2;1} > {1;2;3})`
|
|
372
|
-
* builds. So the offline lexer needs no version gate for operators —
|
|
373
|
-
* anything that reports what a comparison MEANS does.
|
|
376
|
+
* `:put ({2;1} > {1;2;3})` evaluates on 7.24.2 and 7.25beta3 and errors on both
|
|
377
|
+
* long-term builds. So the offline lexer needs no version gate for operators —
|
|
378
|
+
* and anything that reports what a comparison MEANS does.
|
|
374
379
|
*/
|
|
375
380
|
export const ARRAY_COMPARISON_NOTE =
|
|
376
|
-
"array comparison with `>`
|
|
377
|
-
"7.
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
381
|
+
"array comparison with `>` evaluates on 7.24.2 and 7.25beta3 and errors on " +
|
|
382
|
+
"7.23.5 and 7.21.5; the operator table itself is identical on all four. " +
|
|
383
|
+
"**It is not fixed on current long-term**: 7.23.5 is newer than the 7.23.3 " +
|
|
384
|
+
"this was first measured on and still errors, so the change rode the 7.24 " +
|
|
385
|
+
"line and was not backported. The two errors are not the same error: " +
|
|
386
|
+
"7.23.5 says `cannot compare if array is more than array` and 7.21.5 says " +
|
|
387
|
+
"`cannot compare if nothing is more than nothing`, so on 7.21.5 the " +
|
|
388
|
+
"`{2;1}` operands did not survive to the comparison at all. That is a " +
|
|
389
|
+
"RUNTIME difference either way — no version disagrees about what `>` is";
|
|
383
390
|
|
|
384
391
|
const BY_SPELLING: ReadonlyMap<string, RouterosOperator> = new Map(
|
|
385
392
|
OPERATORS.map((entry) => [entry.spelling, entry]),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* separators remain unclassified. A `\<newline>` inside a name interrupts the
|
|
10
10
|
* emitted range so the continuation bytes remain available to the escape fill.
|
|
11
11
|
*
|
|
12
|
-
* Vocabulary
|
|
12
|
+
* Vocabulary settled by #264 B5: `dir` and `cmd` mirror the useful
|
|
13
13
|
* lexical distinction already produced by RouterOS highlight, but are
|
|
14
14
|
* centrs-owned classes.
|
|
15
15
|
*/
|