@tikoci/centrs 0.1.3 → 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 (90) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/GLOSSARY.txt +213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -8
  5. package/commands/AGENTS.md +7 -1
  6. package/commands/api/README.md +19 -18
  7. package/commands/api/examples.md +15 -0
  8. package/commands/btest/README.md +21 -32
  9. package/commands/check/README.md +6 -1
  10. package/commands/discover/README.md +5 -11
  11. package/commands/execute/README.md +41 -29
  12. package/commands/execute/examples.md +41 -3
  13. package/commands/explain/README.md +1716 -0
  14. package/commands/explain/examples.md +606 -0
  15. package/commands/retrieve/README.md +25 -27
  16. package/commands/retrieve/examples.md +16 -0
  17. package/commands/transfer/README.md +28 -20
  18. package/commands/transfer/examples.md +29 -0
  19. package/docs/CLI.md +353 -0
  20. package/docs/CONSTITUTION.md +66 -15
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -0
  23. package/docs/errors/quickchr/machine-not-found.md +22 -0
  24. package/docs/errors/quickchr/machine-stopped.md +20 -0
  25. package/docs/errors/quickchr/package-unavailable.md +27 -0
  26. package/docs/errors/quickchr/unsupported-via.md +35 -0
  27. package/docs/errors/quickchr/unsupported.md +35 -0
  28. package/docs/errors/routeros/version-unsupported.md +26 -0
  29. package/docs/errors/transport/provider-trust.md +29 -0
  30. package/docs/errors/usage/stdin-ignored.md +28 -0
  31. package/docs/errors/validation/unknown-attribute.md +37 -4
  32. package/package.json +45 -10
  33. package/src/api-fanout.ts +29 -14
  34. package/src/api.ts +97 -54
  35. package/src/cli/api.ts +25 -6
  36. package/src/cli/btest.ts +18 -2
  37. package/src/cli/execute.ts +25 -6
  38. package/src/cli/explain.ts +503 -0
  39. package/src/cli/retrieve.ts +33 -1
  40. package/src/cli/selection.ts +71 -11
  41. package/src/cli/settings.ts +2 -2
  42. package/src/cli/transfer.ts +23 -6
  43. package/src/cli.ts +15 -2
  44. package/src/core/envelope.ts +5 -2
  45. package/src/core/error-catalog.ts +40 -0
  46. package/src/core/preflight.ts +306 -0
  47. package/src/core/routeros-string.ts +24 -0
  48. package/src/errors.ts +1 -0
  49. package/src/execute-fanout.ts +29 -14
  50. package/src/execute.ts +114 -63
  51. package/src/explain/arg-tokens.ts +114 -0
  52. package/src/explain/args.ts +1403 -0
  53. package/src/explain/blocks.ts +132 -0
  54. package/src/explain/brace-slots.ts +143 -0
  55. package/src/explain/brace-tokens.ts +38 -0
  56. package/src/explain/catalog.ts +1345 -0
  57. package/src/explain/coordinates.ts +329 -0
  58. package/src/explain/defects.ts +217 -0
  59. package/src/explain/is-known-menu.ts +17 -0
  60. package/src/explain/menus.ts +664 -0
  61. package/src/explain/operator-tokens.ts +310 -0
  62. package/src/explain/operators.ts +458 -0
  63. package/src/explain/path-tokens.ts +150 -0
  64. package/src/explain/pathresolve.ts +1324 -0
  65. package/src/explain/quoted-string.ts +241 -0
  66. package/src/explain/scope-brace.ts +308 -0
  67. package/src/explain/segment.ts +757 -0
  68. package/src/explain/string-tokens.ts +67 -0
  69. package/src/explain/symbol-values.ts +520 -0
  70. package/src/explain/symbols.ts +1597 -0
  71. package/src/explain/token-ranges.ts +72 -0
  72. package/src/explain/transport.ts +497 -0
  73. package/src/explain/value-tokens.ts +80 -0
  74. package/src/explain/values.ts +357 -0
  75. package/src/explain/verbs.ts +64 -0
  76. package/src/explain/verbsplit.ts +813 -0
  77. package/src/explain/write.ts +815 -0
  78. package/src/explain.ts +2152 -0
  79. package/src/index.ts +206 -4
  80. package/src/protocols/mac-telnet-console.ts +61 -17
  81. package/src/resolver/facts.ts +1 -1
  82. package/src/resolver/index.ts +23 -4
  83. package/src/resolver/quickchr-provider.ts +597 -0
  84. package/src/resolver/selection.ts +99 -12
  85. package/src/resolver/service-endpoint.ts +155 -0
  86. package/src/resolver/target.ts +1 -1
  87. package/src/retrieve-fanout.ts +26 -12
  88. package/src/retrieve.ts +81 -18
  89. package/src/transfer-fanout.ts +42 -25
  90. package/src/transfer.ts +87 -44
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Block / scope-brace classification for `explain` (centrs canonicalizer).
3
+ *
4
+ * Ratified by the phase-0 lab, question Q2 (#185) and promoted from the
5
+ * throwaway probe `.scratch/explain-lab-blocktree.ts`. Q2 asks: can an offline,
6
+ * schema-free brace tracker tell a `{…}` SCOPE (`do=`/`else=`/`on-error=`, which
7
+ * IL lowers to `name=;(evl …)`) from a `name={…}` LITERAL value (braces
8
+ * stripped, body kept as RAW text, never lowered)? Descending into a literal
9
+ * body as if it held statements is the prior-art bug (canonicalize-audit #8/H5).
10
+ *
11
+ * The source-visible scope set is a small CLOSED set harvested across the
12
+ * 913-script corpus, so the distinction is schema-free — it reads the argument
13
+ * name (and, for the one head-dependent case, the leading directive) off the
14
+ * statement itself.
15
+ *
16
+ * This module ships only the (single-pass, non-recursive) classification
17
+ * primitives the path resolver needs. The recursive block *tree* / topology
18
+ * surface stays in the lab until it can be promoted with a bounded, index-based
19
+ * traversal of its own.
20
+ *
21
+ * Every string skip here goes through `segment.ts`'s `scanQuotedString` rather
22
+ * than a local quote-to-quote loop, so this module and the segmenter cannot
23
+ * drift on where a string ends: a `}` inside the nested string of a `$[…]`
24
+ * substitution is not a brace, and reading it as one truncated the scope body
25
+ * (#199).
26
+ */
27
+
28
+ import {
29
+ DIRECTIVE_BODY,
30
+ HEAD_SCOPED_ARG_NAMES,
31
+ SCOPE_ARG_NAMES,
32
+ scopeNameFromMasked,
33
+ } from "./scope-brace.ts";
34
+ import { maskComments, scanQuotedString } from "./segment.ts";
35
+
36
+ export { DIRECTIVE_BODY, HEAD_SCOPED_ARG_NAMES, SCOPE_ARG_NAMES };
37
+ /** One depth-0 scope block found in a statement: its name and raw body text. */
38
+ export interface ScopeBlock {
39
+ name: string;
40
+ body: string;
41
+ /**
42
+ * Offset of `body`'s first character within the text `scopeBlocks` was given
43
+ * — i.e. one past the opening `{`.
44
+ *
45
+ * Without it a caller that re-segments `body` gets BODY-RELATIVE offsets and
46
+ * no way to rebase them, so a span or region found inside a `do={…}` cannot
47
+ * be reported in document space. `pathresolve` is that caller.
48
+ */
49
+ start: number;
50
+ }
51
+
52
+ /**
53
+ * The scope name for the `{` at `open`, or null when the brace is a value.
54
+ * Two shapes qualify: `<name>={` with name in the closed set (or head-scoped
55
+ * under the right leading directive), and a brace body directly following a
56
+ * body-taking directive (`:do {`, `:retry {`, `:onerror Err {`).
57
+ */
58
+ export function scopeNameAt(text: string, open: number): string | null {
59
+ return scopeNameFromMasked(maskComments(text), open);
60
+ }
61
+
62
+ /** True when the `{` at `open` opens a scope rather than a literal value. */
63
+ export function isScopeBrace(text: string, open: number): boolean {
64
+ return scopeNameAt(text, open) !== null;
65
+ }
66
+
67
+ /**
68
+ * The depth-0 scope `{…}` in a statement, with names and raw body text. A
69
+ * single left-to-right pass (no recursion): each scope's body is returned raw
70
+ * for the caller to segment/recurse under its own depth budget. Literal `{…}`
71
+ * values are skipped, not descended.
72
+ */
73
+ export function scopeBlocks(text: string): ScopeBlock[] {
74
+ // Scan a comment-masked copy for structure so a `#`-comment `}`/`[` cannot
75
+ // truncate a body or shift depth; slice the ORIGINAL for the body text.
76
+ const masked = maskComments(text);
77
+ const blocks: ScopeBlock[] = [];
78
+ let depth = 0;
79
+ for (let i = 0; i < masked.length; i++) {
80
+ const c = masked[i];
81
+ if (c === '"') {
82
+ i = scanQuotedString(masked, i).end - 1;
83
+ continue;
84
+ }
85
+ if (c === "[" || c === "(") depth++;
86
+ else if (c === "]" || c === ")") {
87
+ if (depth > 0) depth--;
88
+ } else if (c === "{") {
89
+ const end = matchBraceInMasked(masked, i);
90
+ if (depth === 0) {
91
+ const name = scopeNameFromMasked(masked, i);
92
+ if (name !== null)
93
+ blocks.push({
94
+ name,
95
+ body: text.slice(i + 1, end),
96
+ start: i + 1,
97
+ });
98
+ }
99
+ i = end;
100
+ } else if (c === "}") {
101
+ if (depth > 0) depth--;
102
+ }
103
+ }
104
+ return blocks;
105
+ }
106
+
107
+ /** Bodies of the depth-0 scope `{…}` in a statement (for block recursion). */
108
+ export function scopeBodies(text: string): string[] {
109
+ return scopeBlocks(text).map((b) => b.body);
110
+ }
111
+
112
+ function matchBraceInMasked(masked: string, open: number): number {
113
+ let depth = 0;
114
+ for (let i = open; i < masked.length; i++) {
115
+ const c = masked[i];
116
+ if (c === '"') {
117
+ i = scanQuotedString(masked, i).end - 1;
118
+ continue;
119
+ }
120
+ if (c === "{") depth++;
121
+ else if (c === "}") {
122
+ depth--;
123
+ if (depth === 0) return i;
124
+ }
125
+ }
126
+ return masked.length;
127
+ }
128
+
129
+ /** Index of the `}` matching the `{` at `open`, honoring strings and comments. */
130
+ export function matchBrace(text: string, open: number): number {
131
+ return matchBraceInMasked(maskComments(text), open);
132
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Which root-directive value slots read a `{…}` as an ARRAY (#225, #257).
3
+ *
4
+ * `explain` used to gate brace arrays on the statement's resolved path: any
5
+ * `/` path allowed one, positionally or under `in=`. A device sweep showed that
6
+ * is wrong in both directions on CHR 7.23.3 — `:delay {1;2}`, `:beep {1;2}`,
7
+ * `:resolve {1;2}`, `:if condition={1;2}` and `:local name={1;2}` are syntax
8
+ * errors the path rule called arrays, while `:for from=`, `:execute script=`,
9
+ * `:local value=` and 50-odd more are accepted braces it refused.
10
+ *
11
+ * The real key is the SLOT, and a slot has four outcomes, not two:
12
+ *
13
+ * - **array** — the content is EVALUATED: `:foreach i in={(1,2)} do={}`
14
+ * lowers to `in=(, 1 2)`. Only these 72 belong here.
15
+ * - **code** — the braces are a program. `:onerror e in={1;2} do={}` lowers
16
+ * to `in=;(evl (<%% bad command name 1 …))`, so the SAME NAME is an array
17
+ * on `:foreach` and a block on `:onerror`.
18
+ * - **text** — the brace is accepted and its content taken VERBATIM as a
19
+ * script: `:execute script={(1,2)}` lowers to `script=(1,2)`, not
20
+ * `(, 1 2)`. This outcome is why `{1;2}` alone is not a sufficient probe —
21
+ * `:local z {1;2}` and `:execute script={1;2}` both lower to `…=1;2`, and
22
+ * reading only that put `:execute script=` in the array set, one step from
23
+ * hinting `array` on a script body.
24
+ * - **error** — `:parse` refuses the statement.
25
+ *
26
+ * A slot the sweep could not ask (30 of 222, mostly ones whose scalar control
27
+ * also fails) is absent from this table, which is the fail-closed direction:
28
+ * absent means the walk refuses to read the value, and a refusal costs nothing
29
+ * but coverage. Every brace array in the 948-script corpus lands on a slot
30
+ * listed here except one `:execute {…}` script body, which is `text`.
31
+ *
32
+ * A positional slot is keyed by its zero-based index among positionals, because
33
+ * position decides: `:local {1;2}` puts the literal in the NAME slot (a syntax
34
+ * error) while `:local z {1;2}` puts it in the VALUE slot.
35
+ *
36
+ * **The verb is matched VERBATIM, and that is the device's rule, not laziness.**
37
+ * `verbsplit.ts` keeps a run token's source casing, and RouterOS accepts only
38
+ * the lower-case spelling: `:LOCAL z {1;2}`, `:Local z {1;2}`, `:PUT {1;2}` and
39
+ * even `/IP/DNS/set servers=1.1.1.1` all draw `expected command name (line 1
40
+ * column 2)` on CHR 7.23.3, while an ARGUMENT's casing is free (`:local Z {1;2}`
41
+ * parses and binds `$Z`). Normalizing the verb here would open the gate on a
42
+ * statement the device refuses to parse — the exact fabrication this table
43
+ * exists to remove — so a differently-cased directive misses every entry and is
44
+ * refused, which is the same answer the device gives.
45
+ *
46
+ * The evidence is `test/fixtures/explain/values.json` →
47
+ * `interiorGrounding.braceSlots` (222 rows, CHR 7.23.3, `:parse` IL — NOT
48
+ * `highlight`, which accepts `{1;2,}` and `(1,)` that `:parse` rejects), and
49
+ * `explain-values.test.ts` asserts this table still equals the `array` rows of
50
+ * that block. Re-derive with `bun run explain:probe:brace-slots` (#186).
51
+ */
52
+ const BRACE_ARRAY_SLOTS: ReadonlyMap<string, ReadonlySet<string>> = new Map(
53
+ (
54
+ [
55
+ [
56
+ "convert",
57
+ ["#0", "from-scheme", "to-scheme", "transform-scheme", "value"],
58
+ ],
59
+ ["deserialize", ["#0", "value"]],
60
+ ["error", ["#0", "message"]],
61
+ ["find", ["#0", "#1", "from", "in", "key"]],
62
+ ["for", ["from", "step", "to"]],
63
+ ["foreach", ["in"]],
64
+ ["global", ["#1", "value"]],
65
+ ["len", ["#0", "value"]],
66
+ ["local", ["#1", "value"]],
67
+ ["parse", ["#0", "value"]],
68
+ ["pick", ["#0", "#1", "begin", "counter", "end"]],
69
+ ["put", ["#0", "message"]],
70
+ ["range", ["#0", "#1", "from", "to"]],
71
+ ["return", ["#0", "value"]],
72
+ ["rndnum", ["#0", "#1", "from", "to"]],
73
+ ["rndstr", ["#0", "#1", "from", "length"]],
74
+ ["serialize", ["#0", "order", "value"]],
75
+ ["set", ["#1", "value"]],
76
+ ["toarray", ["#0", "value"]],
77
+ ["tobool", ["#0", "value"]],
78
+ ["toid", ["#0", "value"]],
79
+ ["toip", ["#0", "value"]],
80
+ ["toip6", ["#0", "value"]],
81
+ ["tonsec", ["#0", "value"]],
82
+ ["tonum", ["#0", "value"]],
83
+ ["tostr", ["#0", "value"]],
84
+ ["totime", ["#0", "value"]],
85
+ ["typeof", ["#0", "value"]],
86
+ ] as const
87
+ ).map(([verb, slots]) => [verb, new Set<string>(slots)]),
88
+ );
89
+
90
+ /** Slots that accept a brace as code or verbatim script text, not as an array. */
91
+ const BRACE_PROGRAM_SLOTS: ReadonlyMap<string, ReadonlySet<string>> = new Map(
92
+ (
93
+ [
94
+ ["set", ["do"]],
95
+ ["do", ["#0", "#1", "command", "on-error"]],
96
+ ["execute", ["#0", "script"]],
97
+ ["for", ["do", "on-error"]],
98
+ ["foreach", ["do", "on-error"]],
99
+ ["global", ["do"]],
100
+ ["grep", ["#0", "script"]],
101
+ ["if", ["do", "else"]],
102
+ ["local", ["do"]],
103
+ ["onerror", ["#1", "do", "in"]],
104
+ ["retry", ["#0", "#1", "command", "on-error"]],
105
+ ["time", ["#0", "#1", "command"]],
106
+ ["while", ["do"]],
107
+ ] as const
108
+ ).map(([verb, slots]) => [verb, new Set<string>(slots)]),
109
+ );
110
+
111
+ /** The table as flat `verb slot` pairs, for the fixture-drift assertion. */
112
+ export function braceArraySlotPairs(): string[] {
113
+ const pairs: string[] = [];
114
+ for (const [verb, slots] of BRACE_ARRAY_SLOTS)
115
+ for (const slot of slots) pairs.push(`${verb} ${slot}`);
116
+ return pairs.sort();
117
+ }
118
+
119
+ /** The code/text table as flat pairs, for the same fixture-drift assertion. */
120
+ export function braceProgramSlotPairs(): string[] {
121
+ const pairs: string[] = [];
122
+ for (const [verb, slots] of BRACE_PROGRAM_SLOTS)
123
+ for (const slot of slots) pairs.push(`${verb} ${slot}`);
124
+ return pairs.sort();
125
+ }
126
+
127
+ /**
128
+ * Whether a `{…}` in this slot of this root directive is an array literal.
129
+ *
130
+ * `verb` is the resolved directive name without its colon; `slot` is the
131
+ * lowercase argument name, or `#<index>` for a positional.
132
+ */
133
+ export function braceSlotTakesArray(verb: string, slot: string): boolean {
134
+ return BRACE_ARRAY_SLOTS.get(verb)?.has(slot) === true;
135
+ }
136
+
137
+ /** Whether the device sweep proved this root-directive slot accepts `{...}`. */
138
+ export function braceSlotAcceptsBrace(verb: string, slot: string): boolean {
139
+ return (
140
+ braceSlotTakesArray(verb, slot) ||
141
+ BRACE_PROGRAM_SLOTS.get(verb)?.has(slot) === true
142
+ );
143
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * B3 brace fill — claims brace delimiters on the residual.
3
+ *
4
+ * Value-anchored array literals are claimed as `value` leaves, but their outer
5
+ * delimiters remain unclassified (container excluded to avoid overlap). Scope
6
+ * braces (`do={...}` etc.) are never array values. Both families therefore
7
+ * appear on the residual after the `value` and `string` fills, so this fill
8
+ * claims every remaining `{` / `}` byte there as provisional `brace`. Whether
9
+ * scope vs array braces later deserve distinct classes is #264 B5 and does not
10
+ * move byte coverage.
11
+ */
12
+
13
+ import type { ExplainToken } from "../explain.ts";
14
+ import { clipToResidual } from "./token-ranges.ts";
15
+
16
+ /** Brace delimiter spans on the residual. */
17
+ export function braceSpans(
18
+ analyzed: string,
19
+ residual: readonly { start: number; end: number }[],
20
+ ): ExplainToken[] {
21
+ const len = analyzed.length;
22
+ if (len === 0 || residual.length === 0) return [];
23
+ const out: ExplainToken[] = [];
24
+ for (let i = 0; i < analyzed.length; i++) {
25
+ const c = analyzed[i] as string;
26
+ if (c !== "{" && c !== "}") continue;
27
+ for (const r of clipToResidual(i, i + 1, residual)) {
28
+ out.push({
29
+ start: r.start,
30
+ end: r.end,
31
+ class: "brace" as const,
32
+ ev: "e14",
33
+ });
34
+ }
35
+ }
36
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
37
+ return out;
38
+ }