@tikoci/centrs 0.1.4 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +309 -0
  2. package/GLOSSARY.txt +40 -1
  3. package/README.md +8 -4
  4. package/commands/AGENTS.md +7 -4
  5. package/commands/api/AGENTS.md +13 -1
  6. package/commands/api/README.md +37 -8
  7. package/commands/api/examples.md +31 -1
  8. package/commands/execute/README.md +39 -10
  9. package/commands/execute/examples.md +79 -7
  10. package/commands/explain/README.md +1195 -155
  11. package/commands/explain/examples.md +99 -3
  12. package/commands/mcp/README.md +7 -4
  13. package/commands/mcp/examples.md +2 -2
  14. package/commands/terminal/examples.md +7 -1
  15. package/docs/CLI.md +2 -2
  16. package/docs/CONSTITUTION.md +72 -15
  17. package/docs/MATRIX.md +89 -52
  18. package/docs/errors/validation/syntax.md +29 -4
  19. package/package.json +13 -2
  20. package/src/api-fanout.ts +17 -4
  21. package/src/api.ts +254 -67
  22. package/src/cli/api.ts +1 -1
  23. package/src/cli/btest.ts +1 -1
  24. package/src/cli/explain.ts +1 -1
  25. package/src/cli/terminal.ts +1 -1
  26. package/src/core/envelope.ts +53 -0
  27. package/src/core/error-catalog.ts +2 -1
  28. package/src/core/execute-command.ts +148 -0
  29. package/src/core/inspect.ts +36 -4
  30. package/src/core/version.ts +4 -0
  31. package/src/core/yaml.ts +86 -0
  32. package/src/execute-fanout.ts +18 -4
  33. package/src/execute.ts +225 -244
  34. package/src/explain/arg-tokens.ts +62 -25
  35. package/src/explain/args.ts +398 -52
  36. package/src/explain/blocks.ts +54 -11
  37. package/src/explain/brace-tokens.ts +4 -3
  38. package/src/explain/catalog.ts +42 -44
  39. package/src/explain/coordinates.ts +159 -0
  40. package/src/explain/defects.ts +2 -1
  41. package/src/explain/escape-tokens.ts +77 -0
  42. package/src/explain/highlight-projection.ts +308 -0
  43. package/src/explain/menus.ts +9 -5
  44. package/src/explain/operator-tokens.ts +13 -7
  45. package/src/explain/operators.ts +27 -20
  46. package/src/explain/path-tokens.ts +1 -1
  47. package/src/explain/pathresolve.ts +358 -113
  48. package/src/explain/quoted-string.ts +110 -20
  49. package/src/explain/scope-brace.ts +175 -22
  50. package/src/explain/segment.ts +429 -21
  51. package/src/explain/separator.ts +187 -0
  52. package/src/explain/split-owner.ts +194 -0
  53. package/src/explain/string-tokens.ts +14 -4
  54. package/src/explain/symbol-values.ts +209 -173
  55. package/src/explain/symbols.ts +21 -1
  56. package/src/explain/transport.ts +17 -0
  57. package/src/explain/value-tokens.ts +15 -12
  58. package/src/explain/verbsplit.ts +165 -23
  59. package/src/explain/write.ts +47 -6
  60. package/src/explain.ts +522 -72
  61. package/src/index.ts +19 -2
  62. package/src/mcp/server.ts +2 -1
  63. package/src/offline-gate.ts +148 -0
  64. package/src/retrieve-fanout.ts +1 -1
  65. package/src/retrieve.ts +1 -59
  66. package/src/settings.ts +2 -1
  67. package/src/transfer-fanout.ts +1 -1
  68. package/src/transfer.ts +1 -1
@@ -16,14 +16,23 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
16
16
  - Mirror RouterOS `execute` semantics. Input is a CLI string; output is
17
17
  console-shaped text (or structured records for path+verb writes) wrapped in
18
18
  the standard envelope. This includes write-shaped add/set/remove.
19
- - Validation is two-stage (see constitution: validation; grounded on CHR 7.23.3,
19
+ - Validation runs **offline first, then on the device** (see constitution:
20
+ validation; GH#354). Stage 1 is the corpus-gated analyzer over the exact
21
+ command string, with no connection: a syntax fault is rejected with a byte
22
+ span in `error.context.span` and the adapter is never dialed, and
23
+ `meta.validation.stages[]` then shows the device stage `skipped`. Stage 1
24
+ cannot see an unknown attribute — it has no per-menu schema — so stage 2 is
25
+ not optional.
26
+ - Stage 2 is itself two gates (grounded on CHR 7.23.3,
20
27
  GH#230): a single `:put [:parse "<cmd>"]` covers both the syntax and the
21
28
  unknown-attribute (name-level) gate on **every** transport — `:parse` never
22
29
  throws; its return value's printed form (`(evl …)` / `(evl bad parameter …)` /
23
30
  `(<%% bad command name …)`) is read from the REST/native `ret` *and* the
24
31
  console output. That check is necessary, not sufficient: value-type errors pass
25
- it, and the `/console/inspect` / server re-validation semantic checks are
26
- separate.
32
+ it, and the `/console/inspect request=child` / server re-validation semantic
33
+ checks are separate. Command-level `request=completion` is deliberately not
34
+ used: it hangs REST and closes native API on RouterOS 7.12.2 (GH#343), while
35
+ `child` returns the complete argument-name set immediately.
27
36
  The outer RouterOS string literal escapes backslash, quote, and dollar so
28
37
  validation parses the caller's exact command rather than expanding variables
29
38
  in the wrapper.
@@ -73,13 +82,33 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
73
82
  terminal width, so each transport handles width differently. **mac-telnet** is
74
83
  the only one that negotiates a size: `src/protocols/mac-telnet-console.ts`
75
84
  answers the device's terminal-size probe with a deliberately tall/wide screen,
76
- so captured columns are neither wrapped nor paginated. **SSH execute** runs
77
- *without* a pseudo-tty (`src/protocols/ssh.ts`), so RouterOS emits no
78
- terminal-wrapped output to begin with — there is nothing to negotiate.
79
- **REST/native-api** are not consoles, so terminal width never applies (they
80
- return structured records, or a string `ret` for script-mode). There is
81
- therefore no per-call `--width` knob today — an explicit width override is
82
- deferred until a concrete need appears.
85
+ so captured columns are neither paginated nor wrapped *at the negotiated
86
+ width*. **SSH execute** runs *without* a pseudo-tty (`src/protocols/ssh.ts`),
87
+ so no width is negotiated at all. **REST/native-api** are not consoles, so a
88
+ terminal width never applies (they return structured records, or a string
89
+ `ret` for script-mode). There is therefore no per-call `--width` knob today —
90
+ an explicit width override is deferred until a concrete need appears.
91
+ - **A device-side wrap that no width negotiation reaches (GH#352).** Declaring a
92
+ width is not sufficient, because RouterOS can wrap for reasons that are not
93
+ about width. On 7.23.6 / 7.24.3 / 7.25beta4 a settings menu with exactly one
94
+ property prints its value **one character per line**: `/system/identity/print`
95
+ returns `name: C` / `H` / `R` where 7.24.2 returns `name: CHR`. Measured on
96
+ stock CHR, the wrap is identical over REST `POST /rest/execute`, over
97
+ `ssh user@host "<cmd>"`, and over the mac-telnet console — including the
98
+ mac-telnet reader that has already answered the size probe with 512 columns,
99
+ and the ssh client that negotiates nothing. The console login modifiers
100
+ (`+c512w200h`, `+ct`, `+e`) do not change it, and it is not proportional to
101
+ the value's length. Every multi-property `print` is unaffected on the same
102
+ build, as is `:put [/system/identity/get name]`.
103
+
104
+ Consequence for this command: `data.ret` carries the device's console text
105
+ **verbatim**, wrap included — centrs cannot tell a layout newline from
106
+ content, and inventing a de-wrapper would corrupt legitimate output. Callers
107
+ that need a value rather than a rendering should read it structurally
108
+ (`retrieve`, or `api`) or ask the device for the scalar
109
+ (`:put [<menu>/get <property>]`). The integration examples compare identities
110
+ with `print` column wrapping collapsed for this reason — see
111
+ `collapsePrintWrapping` in `test/integration/chr.ts`.
83
112
  - Attribute values pass through **verbatim** — centrs does not guess types or
84
113
  coerce them (RouterOS REST tolerates string values; the native API is
85
114
  all-strings). Unlike `jo`-style KV builders, there is no per-value
@@ -73,7 +73,7 @@ arbitrary script expressions. The `$a` variable must survive the validation
73
73
  wrapper: if that outer string expands it before `:parse`, this example fails even
74
74
  though RouterOS accepts the original command.
75
75
 
76
- ### 5. Syntax reject from `:put [:parse ...]`
76
+ ### 5. Syntax reject before the write round-trip
77
77
 
78
78
  Malformed CLI is rejected by the syntax gate before semantic validation or the
79
79
  write round-trip.
@@ -82,9 +82,16 @@ write round-trip.
82
82
  centrs execute $R '/ip/address/add address="unterminated interface=ether1' --via rest-api --username $U --password $P --yes
83
83
  ```
84
84
 
85
- Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
86
- the RouterOS parse error string from `:put [:parse ...]`, `meta.via=rest-api`,
87
- and no address is added.
85
+ Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=rest-api`, and
86
+ no address is added.
87
+
88
+ **Which stage rejects it changed in GH#354.** This input is now caught by the
89
+ *offline* stage, so `error.context.validationStage="offline"`,
90
+ `error.cause="explain/canonicalizer/unterminated-string"`,
91
+ `error.context.span={"start":24,"end":54}`, and the device is never contacted —
92
+ see V1–V3 below. It reached `:put [:parse ...]` before that. The code, the
93
+ `meta.via`, and the no-mutation guarantee are unchanged, which is why the
94
+ assertion in `test/integration/execute.test.ts` still reads `validation/syntax`.
88
95
 
89
96
  ### 6. Semantic reject that `:parse` accepts
90
97
 
@@ -202,14 +209,16 @@ centrs execute $A "/ip/address/remove numbers=$ID" --via native-api --port $API_
202
209
  Envelope: `ok: true`, `meta.via=native-api`. A subsequent retrieve of
203
210
  `/ip/address` does not contain `$ID`.
204
211
 
205
- ### 15. Native syntax reject from `:parse`
212
+ ### 15. Native syntax reject
206
213
 
207
214
  ```bash
208
215
  centrs execute $A '/ip/address/add address="unterminated interface=ether1' --via native-api --port $API_PORT --username $U --password $P --yes
209
216
  ```
210
217
 
211
- Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
212
- the RouterOS parse error string, `meta.via=native-api`, and no address is added.
218
+ Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=native-api`, and
219
+ no address is added. The native twin of example 5, and it moved the same way:
220
+ the offline stage rejects this input now, so the verdict no longer depends on
221
+ the transport at all.
213
222
 
214
223
  ### 16. Native semantic reject after clean parse
215
224
 
@@ -285,6 +294,10 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output an
285
294
  contains the device identity (cross-checked against REST). `meta.validation.source`
286
295
  is `:put [:parse ...] over mac-telnet`.
287
296
 
297
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
298
+ character per line (GH#352), so the comparison collapses `print` column
299
+ wrapping on both sides rather than asserting the device's layout.
300
+
288
301
  ### 21. Write (add) over mac-telnet
289
302
 
290
303
  ```bash
@@ -329,6 +342,10 @@ Envelope: `ok: true`, `meta.via=ssh`, `data.ret` is the cleaned console output a
329
342
  contains the device identity (cross-checked against REST). `meta.validation.source`
330
343
  is `:put [:parse ...] over ssh`.
331
344
 
345
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
346
+ character per line (GH#352), so the comparison collapses `print` column
347
+ wrapping on both sides rather than asserting the device's layout.
348
+
332
349
  ### S2. Multi-line read returns cleaned, column-aligned output
333
350
 
334
351
  ```bash
@@ -356,6 +373,61 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
356
373
  Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
357
374
  and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
358
375
 
376
+ ## Offline gate (stage 1)
377
+
378
+ Validation runs offline before it runs on the device
379
+ ([`docs/CONSTITUTION.md` → Validation is the product](../../docs/CONSTITUTION.md#validation-is-the-product),
380
+ GH#354). These two prove the stage exists and that a stage-1 rejection reaches no
381
+ router. Run by `test/integration/execute.test.ts`.
382
+
383
+ ### V1. Offline reject, with the device stage never asked
384
+
385
+ `:put [` is an unclosed bracket. The old local quote preflight counted quotes
386
+ only, so this used to cost a full round trip to be told no; the analyzer rejects
387
+ it with a byte span before anything is dialed.
388
+
389
+ ```bash
390
+ centrs execute $R ':put [' --via rest-api --username $U --password $P --json
391
+ ```
392
+
393
+ Envelope: `ok: false`, `error.code=validation/syntax`,
394
+ `error.context.validationStage="offline"`,
395
+ `error.context.span={"start":5,"end":6}`,
396
+ `error.context.diagnostics[0].code="explain/canonicalizer/unclosed"`,
397
+ `error.position` absent (the offline byte offset is never dressed up as a
398
+ RouterOS-reported one), `meta.validation.source` is the offline analyzer, and
399
+ `meta.validation.stages` is `offline: failed` followed by `device: skipped`.
400
+
401
+ ### V2. The same rejection with no reachable router at all
402
+
403
+ The proof that stage 1 opens no connection: point the same command at a port
404
+ nothing is listening on. A gate that dialed first would report a
405
+ `transport/*` error or hang until the timeout.
406
+
407
+ ```bash
408
+ centrs execute 127.0.0.1:1 ':put [' --via rest-api --username $U --password $P --json
409
+ ```
410
+
411
+ Envelope: identical to V1 — `ok: false`, `error.code=validation/syntax`,
412
+ `error.context.validationStage="offline"` — and **not** a `transport/*` code.
413
+
414
+ ### V3. `--validate=false` disables the offline stage too
415
+
416
+ One flag, one meaning. The escape hatch has to reach stage 1, or a caller hitting
417
+ an analyzer defect would have no way past it. RouterOS then rejects the command
418
+ itself on the run.
419
+
420
+ ```bash
421
+ centrs execute $R ':put [' --via rest-api --username $U --password $P --json --validate=false
422
+ ```
423
+
424
+ Envelope: `ok: false`, `error.code=routeros/request-failed` with RouterOS's own
425
+ `syntax error (line 1 column 7)` in the summary — the **device** rejecting it, not
426
+ centrs — plus `meta.validation.enabled=false`, `meta.validation.result="skipped"`,
427
+ and `meta.validation.stages` listing **both** stages as `skipped`. A gated
428
+ surface always reports every stage, so a consumer reading `stages` never has to
429
+ special-case the disabled shape.
430
+
359
431
  ## Target selection (fan-out)
360
432
 
361
433
  These exercise the shared target-selection grammar