@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.
- package/CHANGELOG.md +309 -0
- package/GLOSSARY.txt +40 -1
- package/README.md +8 -4
- package/commands/AGENTS.md +7 -4
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +37 -8
- package/commands/api/examples.md +31 -1
- package/commands/execute/README.md +39 -10
- package/commands/execute/examples.md +79 -7
- package/commands/explain/README.md +1195 -155
- package/commands/explain/examples.md +99 -3
- package/commands/mcp/README.md +7 -4
- package/commands/mcp/examples.md +2 -2
- package/commands/terminal/examples.md +7 -1
- package/docs/CLI.md +2 -2
- package/docs/CONSTITUTION.md +72 -15
- package/docs/MATRIX.md +89 -52
- package/docs/errors/validation/syntax.md +29 -4
- package/package.json +13 -2
- package/src/api-fanout.ts +17 -4
- package/src/api.ts +254 -67
- package/src/cli/api.ts +1 -1
- package/src/cli/btest.ts +1 -1
- 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 +2 -1
- package/src/core/execute-command.ts +148 -0
- package/src/core/inspect.ts +36 -4
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/execute-fanout.ts +18 -4
- package/src/execute.ts +225 -244
- 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 +19 -2
- package/src/mcp/server.ts +2 -1
- package/src/offline-gate.ts +148 -0
- package/src/retrieve-fanout.ts +1 -1
- package/src/retrieve.ts +1 -59
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +1 -1
- package/src/transfer.ts +1 -1
|
@@ -4,9 +4,10 @@ Each numbered example is an executable spec. Once `explain` is implemented,
|
|
|
4
4
|
offline examples run under `test/unit/explain.test.ts` and live examples under
|
|
5
5
|
`test/integration/explain.test.ts` (rest-api) and
|
|
6
6
|
`test/integration/native-api-explain.test.ts` (native-api) against a CHR booted by
|
|
7
|
-
`@tikoci/quickchr` — one
|
|
8
|
-
The offline examples are **green**
|
|
9
|
-
`transport`/`--curl` assertions.
|
|
7
|
+
`@tikoci/quickchr` — one named test per example (example N ↔ named test N); a
|
|
8
|
+
named test may carry several assertions. The offline examples are **green**
|
|
9
|
+
as of #202c-2, including 1, 2, 6 and 23's `transport`/`--curl` assertions.
|
|
10
|
+
The live examples are still the **target**:
|
|
10
11
|
those cells are `designed`, and flag and field names track the ratified surface
|
|
11
12
|
in `README.md`.
|
|
12
13
|
|
|
@@ -604,3 +605,98 @@ No members are emitted for a bare comma run, because whether it splits at all is
|
|
|
604
605
|
the schema's answer. The `(1,2)` spelling is different — its delimiters prove
|
|
605
606
|
the array — so `servers=(1.1.1.1,8.8.8.8)` yields the array plus two `["ip"]`
|
|
606
607
|
members.
|
|
608
|
+
|
|
609
|
+
### 29. A second command-shaped run is a missing separator, not an operand
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
centrs explain '/ip/address add interface=ether1 /ip/route add gateway=192.168.88.1' --json
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
`data.verdict` is `fail` with one
|
|
616
|
+
`explain/canonicalizer/missing-statement-separator` **error** spanning the whole
|
|
617
|
+
second run (`[33,46)` — `/ip/route add`), whose message says to insert `;` or a
|
|
618
|
+
newline before it.
|
|
619
|
+
|
|
620
|
+
The source stays **one** statement: `resolution` is `"resolved"` on
|
|
621
|
+
`/ip/address add`, because that head really was read and the device's own
|
|
622
|
+
`highlight` agrees. What is withdrawn is the reading the run contradicts —
|
|
623
|
+
`command.args` is absent, `arguments.read` is `false`, and `transport.
|
|
624
|
+
classification` is `"unknown"`, so no `centrs execute` line is offered for bytes
|
|
625
|
+
the same result rejects. `data.values.occurrences` is cut at the same byte: it
|
|
626
|
+
keeps `interface` and drops `gateway`, which the device reads as the value of
|
|
627
|
+
`/ip/address/add`'s own `address=` rather than as an attribute name.
|
|
628
|
+
|
|
629
|
+
The rule is a known **menu path** followed by a console **verb**, never "a `/`
|
|
630
|
+
after the verb": `/file remove /flash/skins/foo.html` is legal and lowers to
|
|
631
|
+
`numbers=/flash/skins/foo.html`. It abstains on a menu path with no verb after
|
|
632
|
+
it and on an operand no structure table lists, both of which draw the device's
|
|
633
|
+
generic `bad parameter` and need per-menu positional schema to decide.
|
|
634
|
+
|
|
635
|
+
### 29b. The same commands, separated, read as two
|
|
636
|
+
|
|
637
|
+
```bash
|
|
638
|
+
centrs explain '/ip/address add interface=ether1; /ip/route add gateway=192.168.88.1' --json
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`data.verdict` is `pass` with no diagnostics, two statements, and each keeps its
|
|
642
|
+
own `command.args`. The newline spelling is identical.
|
|
643
|
+
|
|
644
|
+
### 30. A known menu path with arguments does not get an invented verb
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
centrs explain '/ip/firewall/address-list name=ytkids timeout=1h' --json
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
One statement with `resolution: "ambiguous"` and no `command` at all, plus an
|
|
651
|
+
`explain/canonicalizer/ambiguous-statement` **warning** over the whole
|
|
652
|
+
statement naming both premises: `/ip/firewall/address-list` is a known menu,
|
|
653
|
+
and `address-list` is not a console verb. `data.verdict` is `warn`, so the
|
|
654
|
+
default `--fail-on error` still exits `0` — offline is saying it cannot read
|
|
655
|
+
this, not that RouterOS rejects it.
|
|
656
|
+
|
|
657
|
+
It used to answer `resolution: "resolved"`, `command.path: "/ip/firewall"`,
|
|
658
|
+
`command.verb: "address-list"` — a verb assembled out of a path segment. The
|
|
659
|
+
spaced spelling `/ip firewall address-list name=x` was worse, promoting
|
|
660
|
+
`firewall` and reporting the menu as `/ip`. Adding the verb resolves it again
|
|
661
|
+
(`/ip/firewall/address-list add name=ytkids` is `resolved`/`command`), and the
|
|
662
|
+
bare path without arguments is still `resolved`/`menu` (example 18b's rule).
|
|
663
|
+
|
|
664
|
+
### 31. A command substitution inside a string keeps its own tokens
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
centrs explain ':put "$[/ip address print as-value]"' --tokens --json
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
`data.structure.subcommands[0]` resolves `/ip/address` verb `print` — it always
|
|
671
|
+
did — and `data.tokens[]` now agrees with it: `dir` over `/ip` and `address`,
|
|
672
|
+
`cmd` over `print`, with the quote, the `$[`, the separators and the trailing
|
|
673
|
+
`as-value]"` left as `string`. The whole quoted run used to be one opaque
|
|
674
|
+
`string` token, so an editor rendering the partition painted a command centrs
|
|
675
|
+
had already read as flat text.
|
|
676
|
+
|
|
677
|
+
Byte coverage does not change — the partition is still total and gapless — only
|
|
678
|
+
which fill owns those bytes. The `$` sigil and the `[` / `]` delimiters stay
|
|
679
|
+
`string`: #264 B5 kept them there because `structure.subcommands` already
|
|
680
|
+
publishes the resolution and its `innerSpan` locates the brackets around it.
|
|
681
|
+
|
|
682
|
+
### 32. A block binder the envelope cannot publish is still located
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
centrs explain ':if ($x = 1) do={ :put "hi" }' --tokens --json
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
`data.structure.statements[0].arguments` is `{ read: false, why: "a
|
|
689
|
+
substitution or expression value" }` — the strict reading is all-or-nothing and
|
|
690
|
+
a runnable request cannot be rendered from a half-read argument list, so the
|
|
691
|
+
envelope publishes no `ExplainArgumentToken` here at all. `data.tokens[]` still
|
|
692
|
+
locates the argument: `arg` over `do`, `arg-sep` over its `=`, `brace` over the
|
|
693
|
+
`{`.
|
|
694
|
+
|
|
695
|
+
That is why #264 B5 gave the `=` its own class. A consumer holding only the
|
|
696
|
+
envelope would otherwise have to derive the name/value boundary from
|
|
697
|
+
`arguments[].valueSpan.start - 1`, and for 30.8% of the corpus's `=` runs —
|
|
698
|
+
`do=` first among them — there is no published `valueSpan` to derive it from.
|
|
699
|
+
|
|
700
|
+
The same command also shows the two `=` bytes staying apart: the one in
|
|
701
|
+
`($x = 1)` is a comparison and comes back `operator`, decided by fill order
|
|
702
|
+
rather than by a scan for the byte.
|
package/commands/mcp/README.md
CHANGED
|
@@ -80,8 +80,8 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
|
|
|
80
80
|
rejected. RouterOS 7.23 no longer reproduces that exact `blackhole=yes` case,
|
|
81
81
|
but the lesson stands: schema-presence is not parser-acceptance.
|
|
82
82
|
`:put [:parse "<cmd>"]` runs the real parser, and centrs runs **both** `:parse`
|
|
83
|
-
and `/console/inspect`, so the MCP exposes validation as a
|
|
84
|
-
**dry-run** tool (`centrs_validate`) that never mutates.
|
|
83
|
+
and `/console/inspect request=child`, so the MCP exposes validation as a
|
|
84
|
+
first-class **dry-run** tool (`centrs_validate`) that never mutates.
|
|
85
85
|
- **Stateful intents need device-validate-before-apply.** Gold using
|
|
86
86
|
`interface=ether2` fails on a CHR exposing only `ether1`. Pair `execute` with
|
|
87
87
|
cheap readback (`retrieve`).
|
|
@@ -104,7 +104,7 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
|
|
|
104
104
|
| Tool | Core mapping | Reads/Writes | Target source |
|
|
105
105
|
| ---- | ------------ | ------------ | ------------- |
|
|
106
106
|
| `centrs_explain` | `canonicalizeExecuteCommand` | offline, none | none (string only) |
|
|
107
|
-
| `centrs_validate` | execute validation only
|
|
107
|
+
| `centrs_validate` | execute validation only, **no run**: offline analysis, then the device stage — `:put [:parse ...]` alone over ssh/mac-telnet, `:parse` + `/console/inspect` for structured REST/native | read-only | CDB |
|
|
108
108
|
| `centrs_retrieve` | `retrieve` / `retrieveGroup` | read-only | CDB |
|
|
109
109
|
| `centrs_execute` | `execute` | read or write | CDB |
|
|
110
110
|
| `centrs_devices` | `devices` (`list`/`show`/`groups`/`add`/`edit`/`set`/`remove`) | CDB read/write | CDB file |
|
|
@@ -118,7 +118,10 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
|
|
|
118
118
|
- `centrs_validate` is the headline distilled from the bench: a dry-run that runs
|
|
119
119
|
the real parser plus schema inspection against a registered target and returns
|
|
120
120
|
the validation envelope **without executing**. Use it to reject schema-invalid
|
|
121
|
-
commands before any write.
|
|
121
|
+
commands before any write. It shares `execute`'s two-stage gate (constitution:
|
|
122
|
+
validation is the product, GH#354), so a syntax fault comes back from the
|
|
123
|
+
offline stage with a byte span and without contacting the target at all, and
|
|
124
|
+
`meta.validation.stages[]` says which stage spoke.
|
|
122
125
|
- `centrs_retrieve` mirrors `retrieve`, including attribute projection,
|
|
123
126
|
`listAttributes`, and `group` fanout.
|
|
124
127
|
- `centrs_execute` mirrors `execute`. Read-shaped commands run subject only to
|
package/commands/mcp/examples.md
CHANGED
|
@@ -65,8 +65,8 @@ matters for the bench gold-bug in example 4.
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Envelope: `ok: false`, `error.code = "validation/unknown-attribute"` — the
|
|
68
|
-
`/console/inspect` semantic gate rejects the unknown attribute and
|
|
69
|
-
created**. This proves the validate tool fails closed on bad input.
|
|
68
|
+
`/console/inspect request=child` semantic gate rejects the unknown attribute and
|
|
69
|
+
**no route is created**. This proves the validate tool fails closed on bad input.
|
|
70
70
|
|
|
71
71
|
> Bench note: the bench's original gold-bug command
|
|
72
72
|
> `/ip/route/add dst-address=10.99.0.0/24 blackhole=yes` (where `/console/inspect`
|
|
@@ -42,6 +42,9 @@ printf '/system/identity/print\n' | centrs terminal $MAC --via mac-telnet --host
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
stdout contains the device identity (cross-checked against REST). Exit code `0`.
|
|
45
|
+
RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
|
|
46
|
+
character per line (GH#352), so the comparison collapses `print` column
|
|
47
|
+
wrapping on both sides rather than asserting the device's layout.
|
|
45
48
|
|
|
46
49
|
### T2. `--via rest-api` has no terminal capability
|
|
47
50
|
|
|
@@ -99,7 +102,10 @@ analog of T4** (a piped subprocess stdin cannot be a TTY), not in CI.
|
|
|
99
102
|
printf '/system/identity/print\n/quit\n' | centrs terminal 127.0.0.1 --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
stdout contains the device identity (cross-checked against REST).
|
|
105
|
+
stdout contains the device identity (cross-checked against REST).
|
|
106
|
+
RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
|
|
107
|
+
character per line (GH#352), so the comparison collapses `print` column
|
|
108
|
+
wrapping on both sides rather than asserting the device's layout. centrs returns
|
|
103
109
|
ssh's exit code (a no-PTY console closed by EOF can exit non-zero — that is the
|
|
104
110
|
device/ssh's result, not a centrs failure; a clean `/quit` from a real TTY exits
|
|
105
111
|
`0`).
|
package/docs/CLI.md
CHANGED
|
@@ -114,7 +114,7 @@ Usage: centrs api <router> <endpoint> [flags]
|
|
|
114
114
|
| `--query / --filter` | `<expr>` | RouterOS-side row filter, AND-combined, repeatable: name=value, name!=value, name>value, name<value, name. |
|
|
115
115
|
| `--raw-query` | `<word>` | Verbatim RouterOS query word (repeatable) for OR / absence / stack expressions. |
|
|
116
116
|
| `--attribute / --proplist` | `<a,b>` | Property projection → `.proplist`. |
|
|
117
|
-
| `--raw` | | Strip the envelope; emit bare RouterOS JSON.
|
|
117
|
+
| `--raw` | | Strip the envelope; emit bare RouterOS JSON. Defaults `--validate` to false (an explicit `--validate=true` still runs the gate); does not imply `--yes`. |
|
|
118
118
|
| `--yes` | | Confirm a mutating (non-read) request in non-interactive runs. |
|
|
119
119
|
| `--stream / --listen` | | Follow changes as an NDJSON envelope stream (native-api only; the `/listen` endpoint infers it). Ends with a summary envelope. |
|
|
120
120
|
| `--count` | `<n>` | Stop a `--stream` after N change frames. |
|
|
@@ -223,7 +223,7 @@ Usage: centrs explain '<input>' [flags]
|
|
|
223
223
|
| `--complete` | | Continuation candidates at the cursor. Live evidence — offline emits a tip and enumerates nothing. |
|
|
224
224
|
| `--schema` | | Path enumeration (verbs, args, types, enums). Live evidence — offline emits a tip and enumerates nothing. |
|
|
225
225
|
| `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
|
|
226
|
-
| `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (
|
|
226
|
+
| `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (every byte not claimed by an analyzer is `unclassified`). |
|
|
227
227
|
| `--format` | <text\|json\|yaml> | Output format for the CLI response. |
|
|
228
228
|
| `--json` | | Shortcut for `--format json`. |
|
|
229
229
|
| `--yaml` | | Shortcut for `--format yaml`. |
|
package/docs/CONSTITUTION.md
CHANGED
|
@@ -9,15 +9,22 @@ This file is short on purpose. Resist the urge to grow it.
|
|
|
9
9
|
|
|
10
10
|
## What centrs is
|
|
11
11
|
|
|
12
|
-
A typed library
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
A typed library for RouterOS analysis and **validated** device access, with
|
|
13
|
+
CLI/MCP/TUI/proxy frontends and structured, actionable diagnostics. The
|
|
14
|
+
TypeScript API is the root surface; everything else is an adapter over it.
|
|
15
|
+
The initial consumers are other TIKOCI tools and developers building integrations.
|
|
16
16
|
|
|
17
17
|
The product is *not* "REST/SSH/native API wrappers." The product is
|
|
18
18
|
**validation + structured envelopes + identity resolution** on top of those
|
|
19
19
|
transports. Without those, this is a worse `curl`.
|
|
20
20
|
|
|
21
|
+
`explain` is shared analysis across transports and a useful offline capability
|
|
22
|
+
in its own right. It exposes source structure, facts, uncertainty, and evidence
|
|
23
|
+
without executing the input. Those facts can support future diagnostics in
|
|
24
|
+
other commands' envelopes; that integration is not implemented by declaring
|
|
25
|
+
this boundary. Analysis findings do not grant execution permission or replace
|
|
26
|
+
the runners' validation gates.
|
|
27
|
+
|
|
21
28
|
centrs also speaks a small set of MikroTik **peer protocols** that are *not*
|
|
22
29
|
RouterOS-command transports — currently the **bandwidth test** (`btest`), which
|
|
23
30
|
mirrors `/tool/bandwidth-test` and `/tool/bandwidth-server`. This is a
|
|
@@ -29,8 +36,40 @@ friendly errors, but the "validate a RouterOS-shaped command" gate below does
|
|
|
29
36
|
|
|
30
37
|
## Validation is the product
|
|
31
38
|
|
|
32
|
-
Every RouterOS-shaped
|
|
33
|
-
re-validate-server-side.
|
|
39
|
+
Every RouterOS-shaped operation sent to a device goes through canonicalize →
|
|
40
|
+
validate → run → re-validate-server-side. Analysis-only `explain` never runs
|
|
41
|
+
the analyzed input; offline analysis opens no connection. Validation on the
|
|
42
|
+
runners is not optional polish.
|
|
43
|
+
|
|
44
|
+
The `validate` step itself has **two stages, in this order** (GH#354):
|
|
45
|
+
|
|
46
|
+
1. **Offline** — the same corpus-gated analyzer `explain` publishes, run over
|
|
47
|
+
the input with no connection (`src/offline-gate.ts`). A rejection is a byte
|
|
48
|
+
span and no round trip; the protocol adapter is never dialed. This stage runs
|
|
49
|
+
on every transport, for `execute` and for `api`'s `/execute` script mode.
|
|
50
|
+
2. **Device** — the RouterOS-side probe below: `:put [:parse "…"]` and/or
|
|
51
|
+
`/console/inspect request=child`, depending on the command shape and the
|
|
52
|
+
transport.
|
|
53
|
+
|
|
54
|
+
The stages are not interchangeable and neither subsumes the other. Offline has
|
|
55
|
+
no per-menu schema (decision 3 — no offline schema snapshot), so it cannot see
|
|
56
|
+
an unknown attribute; the device probe is version-fragile (GH#343), so where it
|
|
57
|
+
is unsafe the offline stage is what keeps the gate from degrading to nothing. A
|
|
58
|
+
clean offline pass is **necessary, never sufficient** — `explain` publishes
|
|
59
|
+
`runtimeAcceptance: "not-proven"` for exactly this reason. An offline `warn` is
|
|
60
|
+
an ABSTENTION and passes through to the device stage; only a `fail` rejects.
|
|
61
|
+
|
|
62
|
+
`meta.validation.stages[]` reports what each stage concluded, including a stage
|
|
63
|
+
that was skipped and why. A reader must always be able to tell "the device
|
|
64
|
+
accepted it" from "the device was never asked".
|
|
65
|
+
|
|
66
|
+
`--validate=false` disables **both** stages — one flag, one meaning. A bypass
|
|
67
|
+
that covered only the device stage would leave a caller stranded behind a
|
|
68
|
+
stage-1 defect with no escape at all, and the analyzer is demonstrably wrong in
|
|
69
|
+
both directions today (GH#355 is a live false accept, GH#351 a live false
|
|
70
|
+
reject). That is a reason for the flag to reach both stages, not a license to
|
|
71
|
+
use it: the rule below still holds — a wrong rejection is fixed in the
|
|
72
|
+
validator, with CHR evidence.
|
|
34
73
|
|
|
35
74
|
- `retrieve` and other read-shaped calls validate against
|
|
36
75
|
`/console/inspect request=syntax path=...,print` (path joined by commas) and
|
|
@@ -397,22 +436,38 @@ default), so key auth is the normal sftp path.
|
|
|
397
436
|
|
|
398
437
|
## Done definition
|
|
399
438
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
439
|
+
Done requires executable evidence for the capability being claimed. Device
|
|
440
|
+
operations and live analysis require **green CHR integration tests** against a
|
|
441
|
+
real RouterOS CHR booted by `@tikoci/quickchr`, via `bun run test:integration`.
|
|
442
|
+
Code existing or unit tests passing cannot establish live behavior. This is
|
|
443
|
+
the **normative** done definition; `AGENTS.md` and
|
|
404
444
|
`.github/instructions/done-definition.instructions.md` point here (the latter
|
|
405
445
|
carries the Copilot-workflow procedure for satisfying it).
|
|
406
446
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
447
|
+
Offline analysis is `verified` when its applicable command examples and
|
|
448
|
+
contract checks pass without a router, its RouterOS semantic claims have
|
|
449
|
+
committed CHR-grounded evidence, and the command's baseline acceptance criteria
|
|
450
|
+
are satisfied. New or changed RouterOS semantics still require live grounding
|
|
451
|
+
with accepted and rejected controls plus a reproducible capture/replay path;
|
|
452
|
+
offline tests do not establish new device facts. Changes to shared execution
|
|
453
|
+
or transport behavior still require `bun run test:integration`. The offline
|
|
454
|
+
call itself never needs a router merely to satisfy a workflow label.
|
|
455
|
+
|
|
456
|
+
`docs/MATRIX.md` owns capability status and the selected work track. A
|
|
457
|
+
capability's acceptance criteria — like `explain`'s Offline baseline
|
|
458
|
+
acceptance — live in its `commands/<name>/README.md`; GitHub issues own the
|
|
459
|
+
tasks and dependencies that satisfy those criteria, not the criteria
|
|
460
|
+
themselves. Umbrella issues index that work rather than replacing capability
|
|
461
|
+
status. Status advances only with matching evidence in the same change.
|
|
462
|
+
Record test names, fixture provenance, and CHR versions where applicable.
|
|
463
|
+
Offline verification never advances a live protocol cell to `CHR-passed`.
|
|
411
464
|
|
|
412
465
|
## Boundaries
|
|
413
466
|
|
|
414
467
|
centrs does:
|
|
415
468
|
|
|
469
|
+
- Analyze RouterOS source independently of a connection, preserving ordered
|
|
470
|
+
structure, source locations, evidence, and ambiguous/unknown readings.
|
|
416
471
|
- Resolve devices/groups from explicit input, env, CDB, MNDP cache.
|
|
417
472
|
- Choose protocols per the table above and configure them.
|
|
418
473
|
- Validate before execute; re-validate server-side immediately before run.
|
|
@@ -425,7 +480,9 @@ centrs does:
|
|
|
425
480
|
|
|
426
481
|
centrs does not:
|
|
427
482
|
|
|
428
|
-
- Replace RouterOS syntax with high-level
|
|
483
|
+
- Replace RouterOS syntax with high-level configuration helpers. Normalized
|
|
484
|
+
representations that preserve its semantics are in scope; reconstructing
|
|
485
|
+
final device state or predicting network behavior is not an offline parse claim.
|
|
429
486
|
- Treat passive discovery (MNDP) as authoritative inventory.
|
|
430
487
|
- Run write-shaped operations without an explicit target and validation policy.
|
|
431
488
|
- Make generated output the hand-edited source of truth.
|
package/docs/MATRIX.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# Matrix
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This file owns **capability status and the selected work track**. The
|
|
4
|
+
command × protocol grid covers live operations; offline analysis, frontends,
|
|
5
|
+
and peer measurement have separate entries below. A capability's acceptance
|
|
6
|
+
criteria live in its `commands/<name>/README.md`; GitHub issues own the tasks
|
|
7
|
+
and dependencies that satisfy them.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
Start with [Current priority](#current-priority), then the command's README,
|
|
10
|
+
examples, and linked issue. Verification follows `docs/CONSTITUTION.md` →
|
|
11
|
+
Done definition. A completed implementation slice is not proof that the whole
|
|
12
|
+
capability is complete.
|
|
9
13
|
|
|
10
14
|
## Cell states
|
|
11
15
|
|
|
@@ -15,10 +19,15 @@ work. See `docs/CONSTITUTION.md` for the cell-state definitions.
|
|
|
15
19
|
| `not-started` | No code and no design |
|
|
16
20
|
| `designed` | `commands/<name>/README.md` describes intent and flags |
|
|
17
21
|
| `coded` | Implementation exists in `src/` |
|
|
22
|
+
| `verified` | Offline capability meets its command README's baseline acceptance criteria in full (examples, contract checks, measured/consumer gates, and CHR-grounded evidence). |
|
|
18
23
|
| `CHR-passed` | Every example in `commands/<name>/examples.md` is green on CHR |
|
|
19
24
|
|
|
20
25
|
A cell advances only with the matching evidence in the same change.
|
|
21
26
|
|
|
27
|
+
`verified` applies to offline capabilities; it does not advance a live protocol
|
|
28
|
+
cell. Existing `CHR-passed` fixture-backed `devices`/`settings` entries retain
|
|
29
|
+
their command-specific evidence contracts.
|
|
30
|
+
|
|
22
31
|
## Grid
|
|
23
32
|
|
|
24
33
|
| Command | rest-api | native-api | ssh | mac-telnet | snmp | mndp | romon | winbox-terminal |
|
|
@@ -37,8 +46,8 @@ A cell advances only with the matching evidence in the same change.
|
|
|
37
46
|
## Status pointers
|
|
38
47
|
|
|
39
48
|
Detailed command contracts live in `commands/<name>/README.md`; example evidence
|
|
40
|
-
lives in `commands/<name>/examples.md` and the named
|
|
41
|
-
|
|
49
|
+
lives in `commands/<name>/examples.md` and the named tests. Keep task history
|
|
50
|
+
and implementation sequencing in the linked issues.
|
|
42
51
|
|
|
43
52
|
- `devices` is transport-less, so its row stays `—`; command state is
|
|
44
53
|
`CHR-passed` by fixture-backed integration tests in `test/integration/devices.test.ts`.
|
|
@@ -60,42 +69,13 @@ section short enough that the grid remains the status surface.
|
|
|
60
69
|
`winbox-terminal` stay `not-started`. Its per-host fan-out is the machinery the
|
|
61
70
|
IP-scan discovery (#149) will iterate; the L2-default timing evidence (#136) is
|
|
62
71
|
recorded in the README before the cells advance past `designed`.
|
|
63
|
-
- `explain`
|
|
64
|
-
|
|
65
|
-
spans/diagnostics, transport classification (api-able vs execute, `curl`
|
|
66
|
-
rendering), and live schema/completion facts. Its offline mode is the
|
|
67
|
-
canonicalizer only and opens no protocol connection (evidence is
|
|
68
|
-
unit/fixture tests); the
|
|
69
|
-
live probes (`/console/inspect` + `:parse`) ride rest-api/native-api, so
|
|
70
|
-
those two cells are `designed`. The surface was decided in the 2026-07-19
|
|
71
|
-
design round (#90) and the spec is **ratified** on the phase-0 grounding
|
|
72
|
-
lab (#185, ratification PR #187). Phase 0.5 has since promoted the ratified
|
|
73
|
-
lab logic into the library — `src/explain/{segment,blocks,coordinates,defects,
|
|
74
|
-
pathresolve,verbsplit,write,symbols,menus,verbs,args,values,brace-slots}.ts`
|
|
75
|
-
with frozen fixtures under `test/fixtures/explain/` — and phase 1 has composed
|
|
76
|
-
them into one analysis and envelope in `src/explain.ts` (`explainCommand` /
|
|
77
|
-
`explainEnvelope`, #202a). **`centrs explain '<input>'` is a real command as
|
|
78
|
-
of #202b** (`src/cli/explain.ts`), and #202c adds statement argument reading,
|
|
79
|
-
the closed Q8 transport classifier, equivalent centrs invocations, and
|
|
80
|
-
opt-in `--curl`. All offline examples, including 1, 2, 6 and 23, run green in
|
|
81
|
-
`test/unit/explain.test.ts`. The row nonetheless stays `designed`: offline
|
|
82
|
-
mode occupies no protocol cell, and the two `designed` cells are the live
|
|
83
|
-
`/console/inspect` + `:parse` probes, which are phase 2. The live grammar
|
|
72
|
+
- `explain`'s two `designed` protocol cells are the future live probes
|
|
73
|
+
(`/console/inspect` + `:parse`, #236). The live grammar
|
|
84
74
|
(`explain <router> '<input>'`) is parsed and refused with
|
|
85
|
-
`usage/not-implemented
|
|
86
|
-
|
|
87
|
-
`src/explain/menus.ts` and `src/explain/catalog.ts`
|
|
88
|
-
|
|
89
|
-
restraml typed trees) and the path catalog (#228, those trees unioned with
|
|
90
|
-
MikroTik's published CLI Reference, carrying kind, per-entry provenance and
|
|
91
|
-
the published applicability gate). Regenerate with `bun run explain:menus` /
|
|
92
|
-
`bun run explain:catalog`; both are drift-checked in QA. They keep the offline
|
|
93
|
-
analyzer offline; do not hand-edit them. The catalog's **command axis is read**
|
|
94
|
-
by `pathresolve.ts`, `verbsplit.ts` and `write.ts` (#228 step 2); #235 also
|
|
95
|
-
reads its menu/settings kind, unioned with `menus.ts` behind
|
|
96
|
-
`src/explain/is-known-menu.ts`, for both a context-applied relative bare path
|
|
97
|
-
and an absolute one. Both changes are inside the offline canonicalizer, so
|
|
98
|
-
this grid is unchanged by them.
|
|
75
|
+
`usage/not-implemented`, never degraded to an offline run. Offline analysis
|
|
76
|
+
has its own capability entry below. The generated structure tables
|
|
77
|
+
`src/explain/menus.ts` and `src/explain/catalog.ts` keep analysis offline;
|
|
78
|
+
their ownership and regeneration contracts live in `commands/explain/README.md`.
|
|
99
79
|
- `transfer / ssh` means the SFTP-backed transfer method. Deferred file-transfer
|
|
100
80
|
methods such as `scp`, `fetch`, and `ftp` are tracked in
|
|
101
81
|
`commands/transfer/README.md`; `fetch` is not a grid column.
|
|
@@ -108,9 +88,20 @@ section short enough that the grid remains the status surface.
|
|
|
108
88
|
- Transport wire-format and delivery caveats belong in `src/protocols/*.ts`
|
|
109
89
|
module headers. Do not expand this file with per-packet or per-router findings.
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
### Offline analysis
|
|
92
|
+
|
|
93
|
+
This capability opens no protocol connection. It is usable independently and
|
|
94
|
+
provides shared analysis for future integrations across the command grid.
|
|
95
|
+
|
|
96
|
+
| Capability | State | Contract and evidence | Work |
|
|
97
|
+
| ---------- | ----- | --------------------- | ---- |
|
|
98
|
+
| explain / offline baseline | `verified` | `commands/explain/README.md` → Offline baseline acceptance; offline examples in `test/unit/explain.test.ts`, focused `test/unit/explain-*` suites, and CHR captures under `test/fixtures/explain/`. The token partition is scored against device `highlight` by `test/unit/explain-highlight-agreement.test.ts` and `test/fixtures/explain/highlight-agreement.json`. The public entry is `@tikoci/centrs/explain`; its dependency boundary and executed browser-consumer proof are gated by `test/unit/explain-browser-entry.test.ts` (`bun run explain:browser-consumer`). #322's fixed-size 16× depth guard and deterministic structural-work guards close the bounded-execution criterion. | #90 indexes correctness, token agreement (#264/#263), consumer verification, and bounded execution (#322). |
|
|
99
|
+
|
|
100
|
+
The CLI/library expose structure, diagnostics, token partitions, symbols, and
|
|
101
|
+
flow-sensitive value facts. `verified` records the offline acceptance evidence;
|
|
102
|
+
it is not a parser-coverage score and does not advance either live protocol cell.
|
|
103
|
+
Live probes (#236), MCP response alignment (#223), and future configuration
|
|
104
|
+
policies are distinct work, not prerequisites for using offline analysis.
|
|
114
105
|
|
|
115
106
|
### Transport-base readiness
|
|
116
107
|
|
|
@@ -126,8 +117,8 @@ single home for wire-format, auth, and delivery facts.
|
|
|
126
117
|
|
|
127
118
|
### Frontend surfaces (orthogonal to the command grid)
|
|
128
119
|
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
Frontends adapt the shared analysis and device-operation core and carry their
|
|
121
|
+
own state, tracked here:
|
|
131
122
|
|
|
132
123
|
| Surface | State | Spec | Notes |
|
|
133
124
|
| ------- | ----- | ---- | ----- |
|
|
@@ -157,11 +148,57 @@ its own capability axis and is exempt from the RouterOS command-validation gate.
|
|
|
157
148
|
Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
|
|
158
149
|
`src/protocols/btest*.ts` / `src/protocols/ec-srp5.ts` module headers.
|
|
159
150
|
|
|
160
|
-
##
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
## Current priority
|
|
152
|
+
|
|
153
|
+
**Tightening validation as a whole** is the selected track, in the three-step
|
|
154
|
+
order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
|
|
155
|
+
|
|
156
|
+
1. **Done** — fix the offline false rejects so the analyzer can be trusted as a
|
|
157
|
+
gate (#347/#348, shipped in #349; the measured corpus false-reject count went
|
|
158
|
+
4 → 1).
|
|
159
|
+
2. **Done** — front the `execute` / `api` device preflight with the offline
|
|
160
|
+
analyzer (#354). The gate is now two-stage; the contract is in
|
|
161
|
+
[`docs/CONSTITUTION.md` → Validation is the product](CONSTITUTION.md#validation-is-the-product).
|
|
162
|
+
3. **Next** — **live `explain` (#236)**, as stage two of a gate that now exists.
|
|
163
|
+
Start with [the #90 task index](https://github.com/tikoci/centrs/issues/90),
|
|
164
|
+
then follow [`commands/explain/README.md`](../commands/explain/README.md) and
|
|
165
|
+
the live examples in
|
|
166
|
+
[`commands/explain/examples.md`](../commands/explain/examples.md).
|
|
167
|
+
|
|
168
|
+
The offline baseline stays `verified`: #264 closed the measured token vocabulary
|
|
169
|
+
and #322 closed the final bounded-execution gap, including the public depth-axis
|
|
170
|
+
guard.
|
|
171
|
+
|
|
172
|
+
The live pass is the device-backed cross-check on the offline result, not a
|
|
173
|
+
replacement for it. It adds read-only `/console/inspect` and `:parse` evidence,
|
|
174
|
+
the describe/completion facets, and the live `observedType`/`schemaType`
|
|
175
|
+
producers owned with #225. REST and native-api cells advance only when their
|
|
176
|
+
applicable examples pass on CHR via `bun run test:integration`.
|
|
177
|
+
|
|
178
|
+
Open offline follow-ups remain explicit but do not reopen the verified baseline.
|
|
179
|
+
Issue #211 retains the fail-closed unlisted-path decision; #272 owns
|
|
180
|
+
non-authoritative `.scratch/` citation cleanup. #350 (the broad optional `=`,
|
|
181
|
+
schema-dependent and so belonging to step 3), #351 (an unresolvable menu that
|
|
182
|
+
abstains at 2-deep nesting but hard-fails at 3-deep) and #355 (`'` is not a
|
|
183
|
+
RouterOS token — a false ACCEPT the corpus join cannot see) are bounded analyzer
|
|
184
|
+
defects that step 2 now surfaces through the `execute`/`api` gate. #336 and #342 are complete: the
|
|
185
|
+
corpus is repinned to three *complete* builds (long-term 7.23.5, stable 7.24.2,
|
|
186
|
+
development 7.25beta3), and the operator sweep and highlight slice are re-swept
|
|
187
|
+
onto those same builds — plus 7.21.5 retained as an age anchor — so the operator
|
|
188
|
+
axis and the token partition are scored on the same firmware. The #335
|
|
189
|
+
source refresh pins both architectures of those versions beside the 7.10.2 /
|
|
190
|
+
7.16 history anchors and accepts the current CLI-Reference drift. Issue #223 is
|
|
191
|
+
a separate MCP response migration. If live evidence finds a wrong offline
|
|
192
|
+
assertion, give that defect a bounded issue and matching CHR-grounded controls
|
|
193
|
+
rather than folding it silently into the live implementation.
|
|
194
|
+
|
|
195
|
+
Unfinished SNMP, RoMON, TUI, or other protocol/frontend cells do not preempt this
|
|
196
|
+
track. Change this selection when the maintainer selects another track.
|
|
197
|
+
|
|
198
|
+
### Protocol backlog order
|
|
199
|
+
|
|
200
|
+
Retained for protocol work when selected; this is not a dependency chain for
|
|
201
|
+
offline analysis. Per-item detail lives in the linked `commands/<name>/`.
|
|
165
202
|
|
|
166
203
|
1. **retrieve / rest-api** — `CHR-passed` (`commands/retrieve/`).
|
|
167
204
|
2. **CDB resolution + metadata overrides** — `CHR-passed` (`commands/devices/`).
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
# `validation/syntax`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The command syntax was rejected by the validation gate — offline analysis, or
|
|
4
|
+
the RouterOS `:parse` probe.
|
|
5
|
+
|
|
6
|
+
The gate has two stages
|
|
7
|
+
([`docs/CONSTITUTION.md` → Validation is the product](../../CONSTITUTION.md#validation-is-the-product)),
|
|
8
|
+
and `error.context.validationStage` says which one spoke:
|
|
9
|
+
|
|
10
|
+
- **`"offline"`** — centrs rejected the input before any connection was opened,
|
|
11
|
+
and `meta.validation.stages[]` shows the device stage `skipped`: the router was
|
|
12
|
+
never asked. The **corpus-gated analyzer** is the whole of this stage, and
|
|
13
|
+
`error.context.validationSource` names it. `error.context.span` carries the
|
|
14
|
+
offending byte range (end-exclusive) and `error.context.diagnostics[]` the
|
|
15
|
+
analyzer's own `explain/<analyzer>/<slug>` codes.
|
|
16
|
+
|
|
17
|
+
A quote-balance supplement used to sit beside it for the unterminated `"` or
|
|
18
|
+
`'` the analyzer passed, reporting no span. It is gone (GH#355): the analyzer
|
|
19
|
+
rejects a bare `'` itself now, with the byte span, and the supplement's own
|
|
20
|
+
model of `'` as a delimiter was false-rejecting input RouterOS accepts.
|
|
21
|
+
- **absent** — RouterOS itself rejected it, through `:put [:parse "…"]`. When the
|
|
22
|
+
device reported a `(line N column M)`, it is on `error.position`, which is
|
|
23
|
+
RouterOS's authoritative 1-based **byte** column and is never synthesized by
|
|
24
|
+
centrs. An offline rejection never carries `error.position`.
|
|
4
25
|
|
|
5
26
|
## Fix
|
|
6
27
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
28
|
+
Run `centrs explain '<command>'` to see the offending bytes in context, fix the
|
|
29
|
+
RouterOS CLI syntax — quotes and bracketed expressions most often — and retry.
|
|
30
|
+
|
|
31
|
+
`--validate=false` bypasses **both** stages. Use it only to probe a RouterOS edge
|
|
32
|
+
centrs reads wrong, never to work around a rejection you believe is correct: if
|
|
33
|
+
the validator rejects something a real router accepts, the validator is the bug
|
|
34
|
+
(constitution: validation is the product).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tikoci/centrs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"exports": {
|
|
32
32
|
".": "./src/index.ts",
|
|
33
33
|
"./cli": "./src/cli.ts",
|
|
34
|
+
"./explain": "./src/explain.ts",
|
|
34
35
|
"./mcp": "./src/mcp.ts",
|
|
35
36
|
"./tui": "./src/tui.ts",
|
|
36
37
|
"./webproxy": "./src/webproxy.ts",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"lint": "bun run lint:biome && bun run lint:typecheck",
|
|
65
66
|
"lint:git:commit": "bun run lint:biome",
|
|
66
67
|
"lint:git:push": "bun run lint:ci",
|
|
67
|
-
"lint:ci": "bun run lint && bun run docs:cli:check && bun run explain:value-census:readme:check && bun run explain:token-census:readme:check && bun run explain:operator-readme:check && bun run lint:markdownlint && bun run lint:cspell && bun run lint:secretlint",
|
|
68
|
+
"lint:ci": "bun run lint && bun run docs:cli:check && bun run explain:value-census:readme:check && bun run explain:token-census:readme:check && bun run explain:highlight-agreement:readme:check && bun run explain:operator-readme:check && bun run explain:bracket-equals:readme:check && bun run lint:markdownlint && bun run lint:cspell && bun run lint:secretlint",
|
|
68
69
|
"lint:cspell": "cspell lint . --gitignore",
|
|
69
70
|
"lint:cspell:all": "cspell lint . --no-gitignore",
|
|
70
71
|
"lint:markdownlint": "markdownlint-cli2 \"**/*.md\" \"!.scratch/**\"",
|
|
@@ -92,9 +93,18 @@
|
|
|
92
93
|
"docs:cli": "bun run scripts/gen-cli-docs.ts",
|
|
93
94
|
"docs:cli:check": "bun run scripts/gen-cli-docs.ts --check",
|
|
94
95
|
"docs:errors": "bun run scripts/gen-error-pages.ts",
|
|
96
|
+
"explain:bracket-equals": "bun run scripts/explain-bracket-equals.ts",
|
|
97
|
+
"explain:bracket-equals:check": "bun run scripts/explain-bracket-equals.ts --check",
|
|
98
|
+
"explain:bracket-equals:readme": "bun run scripts/explain-bracket-equals.ts --readme",
|
|
99
|
+
"explain:bracket-equals:readme:check": "bun run scripts/explain-bracket-equals.ts --readme --check",
|
|
100
|
+
"explain:browser-consumer": "bun run scripts/explain-browser-consumer.ts",
|
|
95
101
|
"explain:catalog": "bun run scripts/gen-explain-catalog.ts",
|
|
96
102
|
"explain:catalog:check": "bun run scripts/gen-explain-catalog.ts --check",
|
|
97
103
|
"explain:corpus-census": "bun run scripts/explain-corpus-census.ts",
|
|
104
|
+
"explain:highlight-agreement": "bun run scripts/explain-highlight-agreement.ts",
|
|
105
|
+
"explain:highlight-agreement:check": "bun run scripts/explain-highlight-agreement.ts --check",
|
|
106
|
+
"explain:highlight-agreement:readme": "bun run scripts/explain-highlight-agreement.ts --readme",
|
|
107
|
+
"explain:highlight-agreement:readme:check": "bun run scripts/explain-highlight-agreement.ts --readme --check",
|
|
98
108
|
"explain:highlight-slice": "bun run scripts/explain-highlight-slice.ts",
|
|
99
109
|
"explain:menus": "bun run scripts/gen-explain-menus.ts",
|
|
100
110
|
"explain:operator-census": "bun run scripts/explain-operator-census.ts",
|
|
@@ -116,6 +126,7 @@
|
|
|
116
126
|
"explain:value-census:check": "bun run scripts/explain-value-census.ts --check",
|
|
117
127
|
"explain:value-census:readme": "bun run scripts/explain-value-census.ts --readme",
|
|
118
128
|
"explain:value-census:readme:check": "bun run scripts/explain-value-census.ts --readme --check",
|
|
129
|
+
"explain:arg-reach": "bun run scripts/explain-arg-reach-sweep.ts",
|
|
119
130
|
"explain:token-census": "bun run scripts/explain-token-census.ts",
|
|
120
131
|
"explain:token-census:check": "bun run scripts/explain-token-census.ts --check",
|
|
121
132
|
"explain:token-census:readme": "bun run scripts/explain-token-census.ts --readme",
|