@tikoci/centrs 0.1.2 → 0.1.4

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/GLOSSARY.txt +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +55 -11
  5. package/SECURITY.md +3 -3
  6. package/commands/AGENTS.md +7 -1
  7. package/commands/api/README.md +46 -27
  8. package/commands/api/examples.md +124 -6
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +376 -53
  11. package/commands/check/examples.md +390 -0
  12. package/commands/devices/README.md +177 -37
  13. package/commands/devices/examples.md +148 -0
  14. package/commands/discover/README.md +5 -11
  15. package/commands/execute/README.md +73 -26
  16. package/commands/execute/examples.md +131 -9
  17. package/commands/explain/README.md +1716 -0
  18. package/commands/explain/examples.md +606 -0
  19. package/commands/retrieve/README.md +55 -71
  20. package/commands/retrieve/examples.md +56 -9
  21. package/commands/settings/README.md +286 -22
  22. package/commands/settings/examples.md +408 -0
  23. package/commands/terminal/README.md +6 -0
  24. package/commands/transfer/README.md +57 -19
  25. package/commands/transfer/examples.md +95 -0
  26. package/docs/CLI.md +353 -0
  27. package/docs/CONSTITUTION.md +102 -26
  28. package/docs/MATRIX.md +106 -173
  29. package/docs/errors/README.md +5 -1
  30. package/docs/errors/cdb/empty-selection.md +19 -0
  31. package/docs/errors/input/incomplete-gps.md +9 -0
  32. package/docs/errors/input/invalid-altitude.md +9 -0
  33. package/docs/errors/input/invalid-bbox.md +9 -0
  34. package/docs/errors/input/invalid-coordinate.md +9 -0
  35. package/docs/errors/input/invalid-radius.md +9 -0
  36. package/docs/errors/internal/settings-failed.md +9 -0
  37. package/docs/errors/quickchr/machine-not-found.md +22 -0
  38. package/docs/errors/quickchr/machine-stopped.md +20 -0
  39. package/docs/errors/quickchr/package-unavailable.md +27 -0
  40. package/docs/errors/quickchr/unsupported-via.md +35 -0
  41. package/docs/errors/quickchr/unsupported.md +35 -0
  42. package/docs/errors/routeros/version-unsupported.md +26 -0
  43. package/docs/errors/settings/consequential-value.md +26 -0
  44. package/docs/errors/settings/invalid-boolean.md +21 -3
  45. package/docs/errors/settings/invalid-format.md +18 -3
  46. package/docs/errors/settings/invalid-integer.md +18 -3
  47. package/docs/errors/settings/invalid-timeout.md +20 -3
  48. package/docs/errors/settings/invalid-via.md +22 -3
  49. package/docs/errors/settings/reserved-key.md +29 -0
  50. package/docs/errors/settings/skip-env-file-active.md +25 -0
  51. package/docs/errors/settings/unknown-key.md +25 -0
  52. package/docs/errors/transport/provider-trust.md +29 -0
  53. package/docs/errors/usage/fanout-not-supported.md +16 -0
  54. package/docs/errors/usage/stdin-ignored.md +28 -0
  55. package/docs/errors/validation/unknown-attribute.md +37 -4
  56. package/package.json +49 -14
  57. package/src/api-fanout.ts +469 -0
  58. package/src/api.ts +171 -56
  59. package/src/cli/api.ts +156 -13
  60. package/src/cli/btest.ts +38 -4
  61. package/src/cli/common.ts +11 -0
  62. package/src/cli/devices.ts +235 -6
  63. package/src/cli/discover.ts +3 -1
  64. package/src/cli/execute.ts +129 -9
  65. package/src/cli/explain.ts +503 -0
  66. package/src/cli/mcp.ts +8 -1
  67. package/src/cli/retrieve.ts +167 -90
  68. package/src/cli/selection.ts +260 -0
  69. package/src/cli/settings.ts +286 -0
  70. package/src/cli/terminal.ts +22 -7
  71. package/src/cli/transfer.ts +192 -51
  72. package/src/cli.ts +22 -2
  73. package/src/core/envelope.ts +5 -2
  74. package/src/core/error-catalog.ts +95 -0
  75. package/src/core/fanout.ts +143 -2
  76. package/src/core/preflight.ts +306 -0
  77. package/src/core/routeros-string.ts +24 -0
  78. package/src/devices.ts +212 -20
  79. package/src/discover.ts +7 -2
  80. package/src/errors.ts +1 -0
  81. package/src/execute-fanout.ts +453 -0
  82. package/src/execute.ts +216 -68
  83. package/src/explain/arg-tokens.ts +114 -0
  84. package/src/explain/args.ts +1403 -0
  85. package/src/explain/blocks.ts +132 -0
  86. package/src/explain/brace-slots.ts +143 -0
  87. package/src/explain/brace-tokens.ts +38 -0
  88. package/src/explain/catalog.ts +1345 -0
  89. package/src/explain/coordinates.ts +329 -0
  90. package/src/explain/defects.ts +217 -0
  91. package/src/explain/is-known-menu.ts +17 -0
  92. package/src/explain/menus.ts +664 -0
  93. package/src/explain/operator-tokens.ts +310 -0
  94. package/src/explain/operators.ts +458 -0
  95. package/src/explain/path-tokens.ts +150 -0
  96. package/src/explain/pathresolve.ts +1324 -0
  97. package/src/explain/quoted-string.ts +241 -0
  98. package/src/explain/scope-brace.ts +308 -0
  99. package/src/explain/segment.ts +757 -0
  100. package/src/explain/string-tokens.ts +67 -0
  101. package/src/explain/symbol-values.ts +520 -0
  102. package/src/explain/symbols.ts +1597 -0
  103. package/src/explain/token-ranges.ts +72 -0
  104. package/src/explain/transport.ts +497 -0
  105. package/src/explain/value-tokens.ts +80 -0
  106. package/src/explain/values.ts +357 -0
  107. package/src/explain/verbs.ts +64 -0
  108. package/src/explain/verbsplit.ts +813 -0
  109. package/src/explain/write.ts +815 -0
  110. package/src/explain.ts +2152 -0
  111. package/src/index.ts +307 -1
  112. package/src/mcp/config.ts +12 -2
  113. package/src/mcp/tools.ts +137 -2
  114. package/src/protocols/adapter.ts +38 -18
  115. package/src/protocols/index.ts +11 -5
  116. package/src/protocols/mac-telnet-console.ts +61 -17
  117. package/src/resolver/cdb.ts +10 -2
  118. package/src/resolver/comment-kv.ts +58 -4
  119. package/src/resolver/config-file.ts +136 -0
  120. package/src/resolver/facts.ts +78 -0
  121. package/src/resolver/geo.ts +453 -0
  122. package/src/resolver/index.ts +65 -0
  123. package/src/resolver/mac.ts +12 -3
  124. package/src/resolver/quickchr-provider.ts +597 -0
  125. package/src/resolver/selection.ts +391 -0
  126. package/src/resolver/service-endpoint.ts +155 -0
  127. package/src/resolver/settings.ts +54 -5
  128. package/src/resolver/target.ts +21 -3
  129. package/src/retrieve-fanout.ts +239 -133
  130. package/src/retrieve.ts +120 -25
  131. package/src/settings.ts +1078 -0
  132. package/src/terminal.ts +11 -1
  133. package/src/transfer-fanout.ts +544 -0
  134. package/src/transfer.ts +133 -40
@@ -0,0 +1,310 @@
1
+ /**
2
+ * B2 operator fill — claims operator bytes on the residual.
3
+ *
4
+ * This is the first B2 fill of #264. It runs **after** the proof-only spans
5
+ * (`comment` + `variable-*`) and sees only the residual left by them — fill
6
+ * order is structural, not lexical (#290 design decision 1). `/` that is a path
7
+ * separator, `,` that is an argument separator, `=` that is a name separator
8
+ * and `-` that is a hyphen in a name are resolved by who claimed the byte
9
+ * first, not by a smarter operator scanner.
10
+ *
11
+ * Two-hop census note: operator's abstention leaves some bytes `unclassified`
12
+ * that a future path/arg fill will claim. That means the classified percentage
13
+ * jumps twice for those bytes (now `unclassified`→stays, later
14
+ * `unclassified`→`path`/`arg`), which is correct — don't misread the later diff
15
+ * as "operator stole bytes".
16
+ *
17
+ * ## Abstention is the same rule three times
18
+ *
19
+ * A byte that is structurally part of a **path** or an **argument** belongs to
20
+ * a later fill, so this fill leaves it alone. Grounded on the corpus device
21
+ * oracle (`parseil_results.il_text`, the IL RouterOS actually parsed to):
22
+ *
23
+ * 1. **`[` is command substitution, not expression grouping.** `(` opens an
24
+ * expression; `[` opens a *command* and `{` a block/array literal. The IL
25
+ * for `[ /system/identity/get value-name=name ]` is
26
+ * `(evl /system/identity/get value-name=name)` — the `/` are path
27
+ * separators and the `=` is an argument separator, with no division and no
28
+ * comparison node. So the `, / = -` conservatism holds everywhere except
29
+ * directly inside `(`. Measured cost: `find where name="x"` inside `[…]`
30
+ * *does* lower to a real `(= $name x)` node, and those `=` bytes now stay
31
+ * `unclassified` — 224 of the corpus's 1,259 bracket `=` against 1,035 that
32
+ * were plain `arg=value`. Abstaining on all of them beats claiming 82%
33
+ * wrong; a `where`-aware fill can take them later.
34
+ * 2. **Glued after `=` is an argument value.** `in-interface-list=!LAN`,
35
+ * `.id=*2`, `oid=.1.3.6.1.2.1` — the byte after an argument `=` starts the
36
+ * value, never an operator.
37
+ * 3. **Glued into an argument name is a name byte.** `:foreach x in=$list` is
38
+ * `/foreach counter=$x` in the IL with **no** `(in …)` node anywhere — `in=`
39
+ * is an argument name. Same for the dotted names: the IL keeps
40
+ * `security.authentication-types=wpa2-psk` as one name and renders `.id` as
41
+ * the single symbol `$.id`, never a `(. …)` concat.
42
+ *
43
+ * Vocabulary is provisional: one `operator` class for all 26 spellings + the
44
+ * two aliases (`&&`, `||`). Per-operator/per-category legend is #264 B5.
45
+ * `<>` is **not** one token — it re-lexes to `<` then `>` as two tokens.
46
+ * `syntax-meta` is residual and merged, never a source (#255).
47
+ */
48
+
49
+ import type { ExplainToken } from "../explain.ts";
50
+ import { loweredSpellings, routerosOperators } from "./operators.ts";
51
+ import { scanQuotedString } from "./quoted-string.ts";
52
+
53
+ const WORD_OPERATORS = new Set(["and", "or", "in", "any"]);
54
+ /**
55
+ * Spellings that are only operators inside an expression group — everywhere
56
+ * else they are path separators, argument separators or hyphens in a name.
57
+ * `->` is never ambiguous and is deliberately absent.
58
+ */
59
+ const EXPRESSION_ONLY = new Set([",", "/", "=", "-"]);
60
+
61
+ /** The only opener that starts an expression. `[` is a command, `{` a block. */
62
+ const EXPRESSION_OPENER = "(";
63
+ const OPENERS = "([{";
64
+ const CLOSERS = ")]}";
65
+
66
+ const PUNCT_ONLY_DOT_GUARD = ".";
67
+
68
+ function isDigit(c: string): boolean {
69
+ return c >= "0" && c <= "9";
70
+ }
71
+
72
+ function isSpace(c: string): boolean {
73
+ return c === " " || c === "\t" || c === "\r" || c === "\n";
74
+ }
75
+
76
+ // Spellings to scan for. Hard-coded from the grounded table; no regex.
77
+ function buildSpellings(): string[] {
78
+ const ops = routerosOperators().map((o) => o.spelling);
79
+ const aliases = loweredSpellings()
80
+ .filter((e) => e.kind === "alias")
81
+ .map((e) => e.spelling);
82
+ // `<>` is re-lexed, not one token — exclude it.
83
+ const all = [...ops, ...aliases].filter((s) => s !== "<>");
84
+ // Deduplicate, longest first.
85
+ const uniq = [...new Set(all)];
86
+ uniq.sort((a, b) => b.length - a.length || b.localeCompare(a));
87
+ return uniq;
88
+ }
89
+
90
+ const SPELLINGS = buildSpellings();
91
+
92
+ function isWordOperator(spelling: string): boolean {
93
+ return WORD_OPERATORS.has(spelling);
94
+ }
95
+
96
+ /**
97
+ * `=` immediately to the left — everything after an argument `=` is the value
98
+ * (`in-interface-list=!LAN`, `.id=*2`, `oid=.1.3.6.1.2.1`), never an operator.
99
+ */
100
+ function followsArgumentEquals(analyzed: string, start: number): boolean {
101
+ return start > 0 && analyzed[start - 1] === "=";
102
+ }
103
+
104
+ /**
105
+ * The spelling is glued into an argument NAME, which ends at its `=`.
106
+ *
107
+ * Two grounded shapes: a word operator immediately before `=` (`in=$list` in
108
+ * `:foreach`), and a dot that joins name parts (`.id=`, `.passphrase=`,
109
+ * `configuration.ssid=`, `security.authentication-types=`). Deliberately not
110
+ * generalized to every spelling — `<`/`>`/`-` before an `=` have no grounded
111
+ * name shape and generalizing would abstain on real comparisons.
112
+ */
113
+ function insideArgumentName(
114
+ analyzed: string,
115
+ start: number,
116
+ spelling: string,
117
+ ): boolean {
118
+ const after = start + spelling.length;
119
+ if (isWordOperator(spelling)) return analyzed[after] === "=";
120
+ if (spelling !== PUNCT_ONLY_DOT_GUARD) return false;
121
+ // `.` + at least one name character, then the `=` that ends the name.
122
+ let p = after;
123
+ if (p >= analyzed.length || !/[A-Za-z]/.test(analyzed[p] as string))
124
+ return false;
125
+ while (p < analyzed.length && /[A-Za-z0-9._-]/.test(analyzed[p] as string))
126
+ p++;
127
+ return analyzed[p] === "=";
128
+ }
129
+
130
+ function isAllResidual(
131
+ start: number,
132
+ len: number,
133
+ isResidual: (pos: number) => boolean,
134
+ ): boolean {
135
+ for (let p = start; p < start + len; p++) if (!isResidual(p)) return false;
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Operator spans on the residual.
141
+ *
142
+ * `analyzed` is the ASCII-normalized document text; `residual` is the gap set
143
+ * left by earlier fills (already sorted, no overlaps). Every emitted span's
144
+ * bytes are fully inside `residual`, sorted by `start`, non-overlapping, and
145
+ * carry `class: "operator"` + `ev: "e10"`.
146
+ */
147
+ export function operatorSpans(
148
+ analyzed: string,
149
+ residual: readonly { start: number; end: number }[],
150
+ ): ExplainToken[] {
151
+ const len = analyzed.length;
152
+ if (len === 0 || residual.length === 0) return [];
153
+
154
+ // Fast residual membership — residual is sorted.
155
+ function isResidual(pos: number): boolean {
156
+ // Binary search over residual ranges.
157
+ let lo = 0;
158
+ let hi = residual.length - 1;
159
+ while (lo <= hi) {
160
+ const mid = (lo + hi) >> 1;
161
+ const r = residual[mid] as { start: number; end: number };
162
+ if (pos < r.start) hi = mid - 1;
163
+ else if (pos >= r.end) lo = mid + 1;
164
+ else return true;
165
+ }
166
+ return false;
167
+ }
168
+
169
+ const out: ExplainToken[] = [];
170
+ // Delimiter stack, not a depth counter: only `(` opens an expression, so the
171
+ // innermost opener — not the nesting level — decides `EXPRESSION_ONLY`.
172
+ const openers: string[] = [];
173
+ let i = 0;
174
+
175
+ while (i < len) {
176
+ const ch = analyzed[i] as string;
177
+
178
+ // String interior — skip entirely, no delimiter accounting inside.
179
+ if (ch === '"' && isResidual(i)) {
180
+ const scan = scanQuotedString(analyzed, i);
181
+ if (scan.end > i + 1) {
182
+ i = scan.end;
183
+ continue;
184
+ }
185
+ // Unterminated double-quoted string — advance one so the scan makes progress
186
+ i++;
187
+ continue;
188
+ }
189
+
190
+ const residualAt = isResidual(i);
191
+
192
+ // Delimiter tracking — only when the byte itself is residual. A `(` or `[`
193
+ // inside a comment/variable is not residual and does not open anything.
194
+ // This is the conservatism signal for `, / = -`.
195
+ if (residualAt && OPENERS.includes(ch)) {
196
+ openers.push(ch);
197
+ i++;
198
+ continue;
199
+ }
200
+ if (residualAt && CLOSERS.includes(ch)) {
201
+ openers.pop();
202
+ i++;
203
+ continue;
204
+ }
205
+
206
+ if (!residualAt) {
207
+ i++;
208
+ continue;
209
+ }
210
+
211
+ // Everything after an argument `=` is that argument's value — no operator
212
+ // starts there. Checked once per offset, not once per spelling.
213
+ if (followsArgumentEquals(analyzed, i)) {
214
+ i++;
215
+ continue;
216
+ }
217
+
218
+ // Try longest-match operator at i.
219
+ let matched: string | null = null;
220
+ for (const spell of SPELLINGS) {
221
+ if (i + spell.length > len) continue;
222
+ // Quick first-char filter
223
+ if (analyzed[i] !== spell[0]) continue;
224
+ if (analyzed.slice(i, i + spell.length) !== spell) continue;
225
+ if (!isAllResidual(i, spell.length, isResidual)) continue;
226
+
227
+ if (isWordOperator(spell)) {
228
+ const before = i > 0 ? analyzed[i - 1] : undefined;
229
+ const after =
230
+ i + spell.length < len ? analyzed[i + spell.length] : undefined;
231
+ if (before !== undefined && /[A-Za-z0-9._-]/.test(before)) continue;
232
+ if (after !== undefined && /[A-Za-z0-9._-]/.test(after)) continue;
233
+ }
234
+
235
+ if (spell === PUNCT_ONLY_DOT_GUARD) {
236
+ // `1.` variable name — dot glued to left alnum and followed by
237
+ // space / close delimiter / end. (1. 2) -> $1. juxtaposition.
238
+ if (i > 0 && /[A-Za-z0-9]/.test(analyzed[i - 1] as string)) {
239
+ const right = i + 1 < len ? analyzed[i + 1] : undefined;
240
+ if (
241
+ right === undefined ||
242
+ isSpace(right) ||
243
+ right === ")" ||
244
+ right === "]" ||
245
+ right === "}" ||
246
+ right === ";" ||
247
+ right === "," ||
248
+ right === '"' ||
249
+ right === "'"
250
+ ) {
251
+ continue;
252
+ }
253
+ }
254
+ // Tight digit.digit — IP literal 1.2, no spaces.
255
+ if (
256
+ i > 0 &&
257
+ i + 1 < len &&
258
+ isDigit(analyzed[i - 1] as string) &&
259
+ isDigit(analyzed[i + 1] as string)
260
+ ) {
261
+ continue;
262
+ }
263
+ // Time literal (.1) — dot immediate after '(' and before digit.
264
+ if (
265
+ i > 0 &&
266
+ analyzed[i - 1] === "(" &&
267
+ i + 1 < len &&
268
+ isDigit(analyzed[i + 1] as string)
269
+ ) {
270
+ continue;
271
+ }
272
+ // Second byte of `..` — variable `$.` re-lex.
273
+ if (i > 0 && analyzed[i - 1] === ".") continue;
274
+ }
275
+
276
+ if (spell === "/") {
277
+ if (i > 0 && analyzed[i - 1] === "/") continue;
278
+ }
279
+
280
+ // Glued into an argument name (`in=`, `.id=`, `configuration.ssid=`).
281
+ if (insideArgumentName(analyzed, i, spell)) continue;
282
+
283
+ // Expression-only conservatism — leaves `, / = -` for the path/arg
284
+ // fills everywhere except directly inside `(`. `->` is always allowed.
285
+ if (
286
+ EXPRESSION_ONLY.has(spell) &&
287
+ openers[openers.length - 1] !== EXPRESSION_OPENER
288
+ )
289
+ continue;
290
+
291
+ matched = spell;
292
+ break;
293
+ }
294
+
295
+ if (matched !== null) {
296
+ out.push({
297
+ start: i,
298
+ end: i + matched.length,
299
+ class: "operator" as const,
300
+ ev: "e10",
301
+ });
302
+ i += matched.length;
303
+ continue;
304
+ }
305
+
306
+ i++;
307
+ }
308
+
309
+ return out;
310
+ }