@tikoci/centrs 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +417 -1
  2. package/GLOSSARY.txt +40 -1
  3. package/README.md +16 -8
  4. package/commands/AGENTS.md +7 -4
  5. package/commands/api/AGENTS.md +13 -1
  6. package/commands/api/README.md +50 -10
  7. package/commands/api/examples.md +100 -2
  8. package/commands/devices/README.md +10 -1
  9. package/commands/devices/examples.md +33 -2
  10. package/commands/execute/README.md +68 -25
  11. package/commands/execute/examples.md +168 -7
  12. package/commands/explain/README.md +1206 -155
  13. package/commands/explain/examples.md +99 -3
  14. package/commands/mcp/README.md +9 -5
  15. package/commands/mcp/examples.md +2 -2
  16. package/commands/retrieve/examples.md +20 -0
  17. package/commands/terminal/examples.md +7 -1
  18. package/docs/CLI.md +3 -3
  19. package/docs/CONSTITUTION.md +83 -21
  20. package/docs/MATRIX.md +89 -52
  21. package/docs/errors/cdb/create-failed.md +20 -0
  22. package/docs/errors/cdb/file-exists.md +23 -0
  23. package/docs/errors/input/invalid-target.md +25 -3
  24. package/docs/errors/routeros/invalid-value.md +3 -2
  25. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  26. package/docs/errors/validation/syntax.md +29 -4
  27. package/package.json +13 -2
  28. package/src/api-fanout.ts +17 -4
  29. package/src/api.ts +346 -75
  30. package/src/cli/api.ts +1 -1
  31. package/src/cli/btest.ts +1 -1
  32. package/src/cli/devices.ts +16 -3
  33. package/src/cli/explain.ts +1 -1
  34. package/src/cli/terminal.ts +1 -1
  35. package/src/core/envelope.ts +53 -0
  36. package/src/core/error-catalog.ts +16 -1
  37. package/src/core/execute-command.ts +455 -0
  38. package/src/core/inspect.ts +58 -26
  39. package/src/core/routeros-errors.ts +66 -0
  40. package/src/core/version.ts +4 -0
  41. package/src/core/yaml.ts +86 -0
  42. package/src/data/winbox-cdb-write.ts +22 -8
  43. package/src/devices.ts +142 -28
  44. package/src/execute-fanout.ts +19 -5
  45. package/src/execute.ts +247 -295
  46. package/src/explain/arg-tokens.ts +62 -25
  47. package/src/explain/args.ts +398 -52
  48. package/src/explain/blocks.ts +54 -11
  49. package/src/explain/brace-tokens.ts +4 -3
  50. package/src/explain/catalog.ts +42 -44
  51. package/src/explain/coordinates.ts +159 -0
  52. package/src/explain/defects.ts +2 -1
  53. package/src/explain/escape-tokens.ts +77 -0
  54. package/src/explain/highlight-projection.ts +308 -0
  55. package/src/explain/menus.ts +9 -5
  56. package/src/explain/operator-tokens.ts +13 -7
  57. package/src/explain/operators.ts +27 -20
  58. package/src/explain/path-tokens.ts +1 -1
  59. package/src/explain/pathresolve.ts +358 -113
  60. package/src/explain/quoted-string.ts +110 -20
  61. package/src/explain/scope-brace.ts +175 -22
  62. package/src/explain/segment.ts +429 -21
  63. package/src/explain/separator.ts +187 -0
  64. package/src/explain/split-owner.ts +194 -0
  65. package/src/explain/string-tokens.ts +14 -4
  66. package/src/explain/symbol-values.ts +209 -173
  67. package/src/explain/symbols.ts +21 -1
  68. package/src/explain/transport.ts +17 -0
  69. package/src/explain/value-tokens.ts +15 -12
  70. package/src/explain/verbsplit.ts +165 -23
  71. package/src/explain/write.ts +47 -6
  72. package/src/explain.ts +522 -72
  73. package/src/index.ts +21 -2
  74. package/src/mcp/server.ts +2 -1
  75. package/src/mcp/tools.ts +1 -1
  76. package/src/offline-gate.ts +148 -0
  77. package/src/protocols/adapter.ts +12 -1
  78. package/src/protocols/mac-telnet-console.ts +6 -4
  79. package/src/protocols/native-api.ts +29 -3
  80. package/src/resolver/target.ts +32 -12
  81. package/src/retrieve-fanout.ts +1 -1
  82. package/src/retrieve.ts +18 -63
  83. package/src/settings.ts +2 -1
  84. package/src/transfer-fanout.ts +1 -1
  85. package/src/transfer.ts +1 -1
@@ -28,10 +28,16 @@
28
28
  import {
29
29
  DIRECTIVE_BODY,
30
30
  HEAD_SCOPED_ARG_NAMES,
31
+ matchingBraceEnd,
31
32
  SCOPE_ARG_NAMES,
32
33
  scopeNameFromMasked,
33
34
  } from "./scope-brace.ts";
34
- import { maskComments, scanQuotedString } from "./segment.ts";
35
+ import {
36
+ documentRange,
37
+ type MaskedRange,
38
+ maskComments,
39
+ scanQuotedString,
40
+ } from "./segment.ts";
35
41
 
36
42
  export { DIRECTIVE_BODY, HEAD_SCOPED_ARG_NAMES, SCOPE_ARG_NAMES };
37
43
  /** One depth-0 scope block found in a statement: its name and raw body text. */
@@ -64,6 +70,22 @@ export function isScopeBrace(text: string, open: number): boolean {
64
70
  return scopeNameAt(text, open) !== null;
65
71
  }
66
72
 
73
+ /**
74
+ * {@link isScopeBrace} for a reader that already holds the document's mask,
75
+ * with `open` relative to `range` rather than to the whole document.
76
+ *
77
+ * The string form has to mask the text it is given, and a statement's text
78
+ * carries its whole nested `do={…}` subtree — so asking it this question once
79
+ * per brace re-derived a mask over that subtree once per enclosing level
80
+ * (#322). Same answer: the floor is what makes a ranged reading identical to
81
+ * the region read on its own (`scope-brace.ts` → `scopeNameFromMasked`).
82
+ */
83
+ export function isScopeBraceIn(range: MaskedRange, open: number): boolean {
84
+ return (
85
+ scopeNameFromMasked(range.masked, range.start + open, range.start) !== null
86
+ );
87
+ }
88
+
67
89
  /**
68
90
  * The depth-0 scope `{…}` in a statement, with names and raw body text. A
69
91
  * single left-to-right pass (no recursion): each scope's body is returned raw
@@ -71,29 +93,46 @@ export function isScopeBrace(text: string, open: number): boolean {
71
93
  * values are skipped, not descended.
72
94
  */
73
95
  export function scopeBlocks(text: string): ScopeBlock[] {
96
+ return scopeBlocksIn(documentRange(text));
97
+ }
98
+
99
+ /**
100
+ * The same scan over a RANGE of a document, reading the document's own mask.
101
+ *
102
+ * `ScopeBlock.start` stays relative to the range, which is what its contract
103
+ * already promises ("within the text `scopeBlocks` was given") and what a
104
+ * caller rebasing through `Loc` needs. Every other offset here is absolute.
105
+ */
106
+ export function scopeBlocksIn(range: MaskedRange): ScopeBlock[] {
74
107
  // Scan a comment-masked copy for structure so a `#`-comment `}`/`[` cannot
75
108
  // truncate a body or shift depth; slice the ORIGINAL for the body text.
76
- const masked = maskComments(text);
109
+ const { text, masked, start: lo, end: hi } = range;
77
110
  const blocks: ScopeBlock[] = [];
78
111
  let depth = 0;
79
- for (let i = 0; i < masked.length; i++) {
112
+ for (let i = lo; i < hi; i++) {
80
113
  const c = masked[i];
81
114
  if (c === '"') {
82
- i = scanQuotedString(masked, i).end - 1;
115
+ i = scanQuotedString(masked, i, hi).end - 1;
83
116
  continue;
84
117
  }
85
118
  if (c === "[" || c === "(") depth++;
86
119
  else if (c === "]" || c === ")") {
87
120
  if (depth > 0) depth--;
88
121
  } else if (c === "{") {
89
- const end = matchBraceInMasked(masked, i);
122
+ const indexedEnd = range.coordinateIdentity
123
+ ? matchingBraceEnd(masked, i)
124
+ : undefined;
125
+ const end =
126
+ indexedEnd !== undefined && indexedEnd < hi
127
+ ? indexedEnd
128
+ : matchBraceInMasked(masked, i, hi);
90
129
  if (depth === 0) {
91
- const name = scopeNameFromMasked(masked, i);
130
+ const name = scopeNameFromMasked(masked, i, lo);
92
131
  if (name !== null)
93
132
  blocks.push({
94
133
  name,
95
134
  body: text.slice(i + 1, end),
96
- start: i + 1,
135
+ start: i + 1 - lo,
97
136
  });
98
137
  }
99
138
  i = end;
@@ -109,12 +148,16 @@ export function scopeBodies(text: string): string[] {
109
148
  return scopeBlocks(text).map((b) => b.body);
110
149
  }
111
150
 
112
- function matchBraceInMasked(masked: string, open: number): number {
151
+ function matchBraceInMasked(
152
+ masked: string,
153
+ open: number,
154
+ limit: number = masked.length,
155
+ ): number {
113
156
  let depth = 0;
114
- for (let i = open; i < masked.length; i++) {
157
+ for (let i = open; i < limit; i++) {
115
158
  const c = masked[i];
116
159
  if (c === '"') {
117
- i = scanQuotedString(masked, i).end - 1;
160
+ i = scanQuotedString(masked, i, limit).end - 1;
118
161
  continue;
119
162
  }
120
163
  if (c === "{") depth++;
@@ -123,7 +166,7 @@ function matchBraceInMasked(masked: string, open: number): number {
123
166
  if (depth === 0) return i;
124
167
  }
125
168
  }
126
- return masked.length;
169
+ return limit;
127
170
  }
128
171
 
129
172
  /** Index of the `}` matching the `{` at `open`, honoring strings and comments. */
@@ -5,9 +5,10 @@
5
5
  * delimiters remain unclassified (container excluded to avoid overlap). Scope
6
6
  * braces (`do={...}` etc.) are never array values. Both families therefore
7
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.
8
+ * claims every remaining `{` / `}` byte there as `brace`. #264 B5 kept scope
9
+ * and array delimiters in one class: `structure.blocks[]` already locates every
10
+ * scope block, and an array container is the `data.values.occurrences[]` entry
11
+ * that is some other occurrence's `parent`, so the distinction is a join away.
11
12
  */
12
13
 
13
14
  import type { ExplainToken } from "../explain.ts";
@@ -9,23 +9,25 @@
9
9
  * Union of two first-order sources (#228):
10
10
  *
11
11
  * 1. MikroTik's published CLI Reference (`https://manual.mikrotik.com/docs/cli-reference/`),
12
- * 1,070 pages, 1,077 entries — first-order about the definition
12
+ * 1,055 pages, 1,062 entries — first-order about the definition
13
13
  * structs and their build-time gates. Since #285 every page is a leaf whose
14
14
  * slug is the CLI path, so no spelling has to be rewritten to be looked up.
15
- * 2. Four pinned restraml `/console/inspect` trees
15
+ * 2. 6 pinned restraml `/console/inspect` trees
16
16
  * (`https://tikoci.github.io/restraml/`) — first-order about the CLI surface:
17
17
  *
18
18
  * | Tree | Arch | RouterOS | Nodes |
19
19
  * | ---- | ---- | -------- | ----- |
20
20
  * | `7.10.2/extra/inspect.json` | x86 | 7.10.2 | 29,086 |
21
21
  * | `7.16/extra/inspect.json` | x86 | 7.16 | 34,876 |
22
- * | `7.23.2/extra/deep-inspect.x86.json` | x86 | 7.23.2 | 40,595 |
23
- * | `7.24rc2/extra/deep-inspect.arm64.json` | arm64 | 7.24rc2 | 42,690 |
22
+ * | `7.24.4/extra/deep-inspect.x86.json` | x86 | 7.24.4 | 41,345 |
23
+ * | `7.24.4/extra/deep-inspect.arm64.json` | arm64 | 7.24.4 | 42,691 |
24
+ * | `7.25beta5/extra/deep-inspect.x86.json` | x86 | 7.25beta5 | 41,794 |
25
+ * | `7.25beta5/extra/deep-inspect.arm64.json` | arm64 | 7.25beta5 | 43,150 |
24
26
  *
25
27
  * | Kind | Total | `both` | `inspect` | `published` |
26
28
  * | ---- | ----- | ------ | --------- | ----------- |
27
- * | `menu` | 558 | 454 | 54 | 50 |
28
- * | `command` | 450 | 407 | 0 | 43 |
29
+ * | `menu` | 554 | 445 | 65 | 44 |
30
+ * | `command` | 450 | 409 | 0 | 41 |
29
31
  * | `settings` | 116 | 107 | 0 | 9 |
30
32
  *
31
33
  * Zero navigation-vs-command contradictions between the two sources. Generation
@@ -45,7 +47,7 @@
45
47
  *
46
48
  * **Gates conjoin down a path, so read them with {@link effectiveGates}, not
47
49
  * row by row.** A row states only what the publication stated at that entry.
48
- * Read row-wise, 2 published-only paths look ungated; read with
50
+ * Read row-wise, 1 published-only path looks ungated; read with
49
51
  * ancestry, the residue carrying no published explanation for its absence at
50
52
  * all is 1.
51
53
  *
@@ -87,7 +89,7 @@ export interface CatalogEntry {
87
89
  * `path|kind|provenance|package|conditions|syscap` — with trailing empty
88
90
  * columns trimmed. Lower-cased, slash-led and sorted by path.
89
91
  *
90
- * Text rather than ~1,124 object literals so that one path is one line in a
92
+ * Text rather than ~1,120 object literals so that one path is one line in a
91
93
  * review diff, and so the formatter has nothing to re-wrap.
92
94
  */
93
95
  const ROWS = `
@@ -152,7 +154,7 @@ const ROWS = `
152
154
  /certificate/settings|settings|both
153
155
  /certificate/sign|command|both
154
156
  /certificate/sign-certificate-request|command|both
155
- /console|menu|both
157
+ /console|menu|inspect
156
158
  /console/inspect|command|both
157
159
  /console/settings|settings|both
158
160
  /container|menu|both|container
@@ -239,7 +241,7 @@ const ROWS = `
239
241
  /interface/bonding|menu|both
240
242
  /interface/bonding/monitor|command|both
241
243
  /interface/bonding/monitor-slaves|command|both
242
- /interface/bridge|menu|both||MSRP_ENABLE
244
+ /interface/bridge|menu|both
243
245
  /interface/bridge/calea|menu|both
244
246
  /interface/bridge/calea/reset-counters|command|both
245
247
  /interface/bridge/calea/reset-counters-all|command|both
@@ -249,11 +251,6 @@ const ROWS = `
249
251
  /interface/bridge/host|menu|both
250
252
  /interface/bridge/mdb|menu|both
251
253
  /interface/bridge/monitor|command|both
252
- /interface/bridge/msrp|menu|published||MSRP_ENABLE
253
- /interface/bridge/msrp/attributes|menu|published||MSRP_ENABLE
254
- /interface/bridge/msrp/domain|menu|published||MSRP_ENABLE
255
- /interface/bridge/msrp/domain/attributes|menu|published||MSRP_ENABLE
256
- /interface/bridge/msrp/domain/monitor|command|published||MSRP_ENABLE
257
254
  /interface/bridge/msti|menu|both
258
255
  /interface/bridge/msti/monitor|command|both
259
256
  /interface/bridge/nat|menu|both
@@ -280,14 +277,15 @@ const ROWS = `
280
277
  /interface/dot1x/server/state|menu|both||!smips
281
278
  /interface/eoip|menu|both
282
279
  /interface/eoipv6|menu|both
283
- /interface/ethernet|menu|both||i386
280
+ /interface/ethernet|menu|both
284
281
  /interface/ethernet/blink|command|both
285
282
  /interface/ethernet/cable-test|command|both
286
- /interface/ethernet/monitor|command|both||i386
283
+ /interface/ethernet/monitor|command|both
287
284
  /interface/ethernet/poe|menu|published|||(poe or poe-in)
288
285
  /interface/ethernet/poe/monitor|command|published|||(poe or poe-in)
289
286
  /interface/ethernet/poe/power-cycle|command|published|||(poe or poe-in)
290
287
  /interface/ethernet/poe/settings|settings|published|||(poe or poe-in) and poesettings
288
+ /interface/ethernet/pon|menu|both
291
289
  /interface/ethernet/reset-counters|command|both
292
290
  /interface/ethernet/reset-mac-address|command|both
293
291
  /interface/ethernet/switch|menu|both
@@ -329,6 +327,7 @@ const ROWS = `
329
327
  /interface/ethernet/switch/reserved-fdb|menu|published|||musicswitch
330
328
  /interface/ethernet/switch/reset-counters|command|both
331
329
  /interface/ethernet/switch/rule|menu|both|||rbswitch
330
+ /interface/ethernet/switch/rule/reset-counters|command|both|||rbswitch
332
331
  /interface/ethernet/switch/shaper|menu|published|||musicswitch
333
332
  /interface/ethernet/switch/stats|settings|published||!smips|musicswitch
334
333
  /interface/ethernet/switch/trunk|menu|published|||musicswitch
@@ -365,7 +364,7 @@ const ROWS = `
365
364
  /interface/lte/monitor|command|both||!smips
366
365
  /interface/lte/run-modem-update|command|published||!smips
367
366
  /interface/lte/scan|command|both||!smips
368
- /interface/lte/settings|settings|both||!smips, !i386, !mips, !powerpc
367
+ /interface/lte/settings|settings|both||!smips
369
368
  /interface/lte/show-capabilities|command|both||!smips
370
369
  /interface/macsec|menu|both||!smips
371
370
  /interface/macsec/monitor|command|both||!smips
@@ -418,7 +417,7 @@ const ROWS = `
418
417
  /interface/sstp-server/monitor|command|both
419
418
  /interface/sstp-server/server|settings|both
420
419
  /interface/veth|menu|both||!smips|container
421
- /interface/vlan|menu|both||!smips
420
+ /interface/vlan|menu|both
422
421
  /interface/vpls|menu|both||!smips
423
422
  /interface/vpls/monitor|command|both||!smips
424
423
  /interface/vrrp|menu|both
@@ -531,7 +530,7 @@ const ROWS = `
531
530
  /interface/wireless/wds/monitor|command|both|wireless-rep
532
531
  /interface/wireless/wps-client|command|both|wireless-rep
533
532
  /interface/wireless/wps-push-button|command|both|wireless-rep
534
- /interface/xfrm|menu|published
533
+ /interface/xfrm|menu|both
535
534
  /iot|menu|inspect
536
535
  /iot/bluetooth|menu|both|iot
537
536
  /iot/bluetooth/advertisers|menu|both|iot
@@ -602,7 +601,7 @@ const ROWS = `
602
601
  /ip/cloud/advanced|settings|both
603
602
  /ip/cloud/back-to-home-file|menu|both|||cloud-vpn
604
603
  /ip/cloud/back-to-home-file/settings|settings|both|||cloud-vpn
605
- /ip/cloud/back-to-home-file/settings/remove-certificate|command|published|||cloud-vpn
604
+ /ip/cloud/back-to-home-file/settings/remove-certificate|command|both|||cloud-vpn
606
605
  /ip/cloud/back-to-home-user|menu|both|||cloud-vpn
607
606
  /ip/cloud/back-to-home-user/show-client-config|command|both|||cloud-vpn
608
607
  /ip/cloud/force-update|command|both
@@ -674,7 +673,7 @@ const ROWS = `
674
673
  /ip/hotspot/walled-garden/ip|menu|both
675
674
  /ip/hotspot/walled-garden/reset-counters|command|both
676
675
  /ip/hotspot/walled-garden/reset-counters-all|command|both
677
- /ip/ipsec|menu|both
676
+ /ip/ipsec|menu|inspect
678
677
  /ip/ipsec/active-peers|menu|both
679
678
  /ip/ipsec/active-peers/kill-connections|command|both
680
679
  /ip/ipsec/identity|menu|both
@@ -683,7 +682,6 @@ const ROWS = `
683
682
  /ip/ipsec/key|menu|both
684
683
  /ip/ipsec/key/psk|menu|both
685
684
  /ip/ipsec/key/psk/generate|command|both
686
- /ip/ipsec/key/qkd|menu|inspect
687
685
  /ip/ipsec/key/rsa|menu|both
688
686
  /ip/ipsec/key/rsa/export-pub-key|command|both
689
687
  /ip/ipsec/key/rsa/generate-key|command|both
@@ -746,6 +744,7 @@ const ROWS = `
746
744
  /ip/ssh|settings|both
747
745
  /ip/ssh/export-host-key|command|both
748
746
  /ip/ssh/import-host-key|command|both
747
+ /ip/ssh/known-hosts|menu|both
749
748
  /ip/ssh/regenerate-host-key|command|both
750
749
  /ip/tftp|menu|both
751
750
  /ip/tftp/settings|settings|both
@@ -850,7 +849,7 @@ const ROWS = `
850
849
  /password|command|both
851
850
  /port|menu|both
852
851
  /port/remote-access|menu|both
853
- /ppp|menu|both
852
+ /ppp|menu|inspect
854
853
  /ppp/aaa|settings|both
855
854
  /ppp/active|menu|both
856
855
  /ppp/l2tp-secret|menu|both
@@ -874,12 +873,11 @@ const ROWS = `
874
873
  /radius/monitor|command|both
875
874
  /radius/reset-counters|command|both
876
875
  /redo|command|both
877
- /root|menu|published||CONSOLE_DEBUG
878
876
  /root/terminal|menu|published
879
877
  /routing|menu|inspect
880
- /routing/bfd|menu|both||BFD_AUTHENTICATION
878
+ /routing/bfd|menu|inspect
881
879
  /routing/bfd/authentication|menu|published||BFD_AUTHENTICATION
882
- /routing/bfd/configuration|menu|both||BFD_AUTHENTICATION
880
+ /routing/bfd/configuration|menu|both
883
881
  /routing/bfd/session|menu|both
884
882
  /routing/bgp|menu|both||!smips
885
883
  /routing/bgp/advertisements|menu|both||!smips
@@ -897,7 +895,7 @@ const ROWS = `
897
895
  /routing/bgp/vpn|menu|both||!smips
898
896
  /routing/discourse|command|both
899
897
  /routing/fantasy|menu|both
900
- /routing/filter|menu|both
898
+ /routing/filter|menu|inspect
901
899
  /routing/filter/chain|menu|both
902
900
  /routing/filter/community-ext-list|menu|both
903
901
  /routing/filter/community-large-list|menu|both
@@ -913,7 +911,7 @@ const ROWS = `
913
911
  /routing/igmp-proxy|settings|both
914
912
  /routing/igmp-proxy/interface|menu|both
915
913
  /routing/igmp-proxy/mfc|menu|both
916
- /routing/isis|menu|both
914
+ /routing/isis|menu|inspect
917
915
  /routing/isis/instance|menu|both
918
916
  /routing/isis/interface|menu|both
919
917
  /routing/isis/interface-template|menu|both
@@ -921,7 +919,7 @@ const ROWS = `
921
919
  /routing/isis/neighbor|menu|both
922
920
  /routing/nexthop|menu|both
923
921
  /routing/nexthop/dump-dot|command|both
924
- /routing/ospf|menu|both
922
+ /routing/ospf|menu|inspect
925
923
  /routing/ospf/area|menu|both
926
924
  /routing/ospf/area/range|menu|both
927
925
  /routing/ospf/instance|menu|both
@@ -943,7 +941,7 @@ const ROWS = `
943
941
  /routing/pimsm/static-rp|menu|both||!smips
944
942
  /routing/pimsm/uib-g|menu|both||!smips
945
943
  /routing/pimsm/uib-sg|menu|both||!smips
946
- /routing/rip|menu|both
944
+ /routing/rip|menu|inspect
947
945
  /routing/rip/instance|menu|both
948
946
  /routing/rip/interface|menu|both
949
947
  /routing/rip/interface-template|menu|both
@@ -958,7 +956,7 @@ const ROWS = `
958
956
  /routing/rpki/session|menu|both
959
957
  /routing/rule|menu|both
960
958
  /routing/settings|settings|both
961
- /routing/stats|menu|both||!smips
959
+ /routing/stats|menu|inspect
962
960
  /routing/stats/memory|menu|both
963
961
  /routing/stats/origin|menu|both
964
962
  /routing/stats/pcap|menu|both||!smips
@@ -973,7 +971,7 @@ const ROWS = `
973
971
  /snmp/send-trap|command|both
974
972
  /special-login|menu|both
975
973
  /system|menu|inspect
976
- /system/backup|menu|both
974
+ /system/backup|menu|inspect
977
975
  /system/backup/cloud|menu|both
978
976
  /system/backup/cloud/download-file|command|both
979
977
  /system/backup/cloud/remove-file|command|both
@@ -993,11 +991,11 @@ const ROWS = `
993
991
  /system/default-configuration/wps-sync-mode-script|settings|published|||wpssync
994
992
  /system/device-mode|settings|both
995
993
  /system/device-mode/update|command|both
996
- /system/gps|settings|both|gps|mmips
994
+ /system/gps|settings|both|gps
997
995
  /system/gps/monitor|command|both|gps
998
996
  /system/hardware|menu|inspect
999
- /system/health|menu|both||!i386
1000
- /system/health/settings|settings|both||!i386, tile|health and health-settings
997
+ /system/health|menu|both
998
+ /system/health/settings|settings|both||!i386|health and health-settings
1001
999
  /system/health/settings/detect-fans|command|both||!i386|health and health-settings
1002
1000
  /system/history|menu|both
1003
1001
  /system/identity|settings|both
@@ -1015,7 +1013,7 @@ const ROWS = `
1015
1013
  /system/logging/action|menu|both
1016
1014
  /system/logging/action/clear|command|both
1017
1015
  /system/note|settings|both
1018
- /system/ntp|menu|both
1016
+ /system/ntp|menu|inspect
1019
1017
  /system/ntp/client|settings|both
1020
1018
  /system/ntp/client/reset-freq-drift|command|both
1021
1019
  /system/ntp/client/servers|menu|both
@@ -1048,9 +1046,9 @@ const ROWS = `
1048
1046
  /system/reboot|command|both
1049
1047
  /system/regulatory|settings|both
1050
1048
  /system/reset-configuration|command|both
1051
- /system/resource|settings|both||!powerpc, !smips
1049
+ /system/resource|settings|both
1052
1050
  /system/resource/cpu|menu|both
1053
- /system/resource/hardware|menu|both||!powerpc, !smips, i386
1051
+ /system/resource/hardware|menu|both||!powerpc, !smips
1054
1052
  /system/resource/hardware/authorize|command|both||!powerpc, !smips
1055
1053
  /system/resource/hardware/usb-power-reset|command|both||!powerpc, !smips, i386
1056
1054
  /system/resource/hardware/usb-settings|settings|both||!powerpc, !smips
@@ -1060,10 +1058,10 @@ const ROWS = `
1060
1058
  /system/resource/pci|menu|inspect
1061
1059
  /system/resource/usb|menu|inspect
1062
1060
  /system/resource/usb/settings|menu|inspect
1063
- /system/routerboard|settings|both||!i386, !i386, !mipsel, !powerpc
1061
+ /system/routerboard|settings|both||!i386
1064
1062
  /system/routerboard/mode-button|settings|both||!i386
1065
1063
  /system/routerboard/reset-button|settings|both||!i386
1066
- /system/routerboard/settings|settings|both||!i386, !i386
1064
+ /system/routerboard/settings|settings|both||!i386
1067
1065
  /system/routerboard/settings/keep-frequency|command|published||!i386, mipsel
1068
1066
  /system/routerboard/upgrade|command|both||!i386
1069
1067
  /system/routerboard/usb|settings|both||!i386, !i386, !mipsel, !powerpc
@@ -1110,7 +1108,7 @@ const ROWS = `
1110
1108
  /tool/dns-update|command|both
1111
1109
  /tool/e-mail|settings|both
1112
1110
  /tool/e-mail/send|command|both
1113
- /tool/fetch|command|both||arm64
1111
+ /tool/fetch|command|both
1114
1112
  /tool/flood-ping|command|both||!smips
1115
1113
  /tool/graphing|settings|both
1116
1114
  /tool/graphing/interface|menu|both
@@ -1161,7 +1159,7 @@ const ROWS = `
1161
1159
  /tool/traffic-generator/quick|command|both
1162
1160
  /tool/traffic-generator/raw-packet-template|menu|both
1163
1161
  /tool/traffic-generator/start|command|both
1164
- /tool/traffic-generator/stats|menu|both
1162
+ /tool/traffic-generator/stats|menu|inspect
1165
1163
  /tool/traffic-generator/stats/latency-distribution|menu|both
1166
1164
  /tool/traffic-generator/stats/port|menu|both
1167
1165
  /tool/traffic-generator/stats/raw|menu|both
@@ -1320,7 +1318,7 @@ export interface CatalogGate {
1320
1318
  * parent's `syscap` applies even though the child entry states none: the gates
1321
1319
  * up a path CONJOIN, they do not override.
1322
1320
  *
1323
- * Read row-wise, 2 published-only paths look ungated; read with
1321
+ * Read row-wise, 1 published-only path looks ungated; read with
1324
1322
  * ancestry the residue is 1. The gap between the two is the
1325
1323
  * conjunction, and it is what a caller has to reproduce: a child's silence about
1326
1324
  * a gate is not the absence of one. #228's finding — that a published-only path
@@ -87,9 +87,31 @@ export interface CoordinateAnalysis {
87
87
  /** Pure-ASCII surface; `analyzed.length === originalU8.length`. */
88
88
  analyzed: Uint8Array;
89
89
  originalU8: Uint8Array;
90
+ /**
91
+ * One run per code point.
92
+ *
93
+ * On the {@link CoordinateAnalysis.ascii} path this array is built ON FIRST
94
+ * READ, not by `analyzeCoordinates`: every caller that only needs an offset
95
+ * or a position can answer from arithmetic there, and materializing one
96
+ * object per character was the single largest allocation in the analyzer
97
+ * (#322). Reading it is always sound — the contract is the array, and the
98
+ * lazy path builds the same one the eager path would have.
99
+ */
90
100
  runs: CharRun[];
91
101
  /** Byte offset of each line's first character (index === 0-based line). */
92
102
  lineStarts: number[];
103
+ /**
104
+ * True when every code point of `original` is ASCII, so `analyzed`,
105
+ * `originalU8` and `original` agree byte for byte and a byte offset IS a
106
+ * UTF-16 offset.
107
+ *
108
+ * This is the property the three coordinate spaces were defined to make
109
+ * checkable, and it is what lets a caller skip the mapping rather than
110
+ * re-derive an identity one. It is a FACT about the input, never a mode: the
111
+ * non-ASCII path below is unchanged and stays the only reading of a
112
+ * non-ASCII document.
113
+ */
114
+ ascii: boolean;
93
115
  }
94
116
 
95
117
  /** UTF-8 byte length of a Unicode scalar value. */
@@ -114,12 +136,34 @@ function utf8Encode(cp: number): number[] {
114
136
  ];
115
137
  }
116
138
 
139
+ /**
140
+ * Any code point outside ASCII.
141
+ *
142
+ * `u`-mode, so the test is over CODE POINTS: an astral character is one match
143
+ * rather than two halves, and a lone surrogate — which is not a scalar value
144
+ * and therefore not ASCII either — matches as itself. That is the same
145
+ * population the walk below normalizes, so the two cannot disagree about what
146
+ * "all ASCII" means.
147
+ */
148
+ const NON_ASCII = /[^\p{ASCII}]/u;
149
+
117
150
  /**
118
151
  * Build the coordinate analysis of `original`: its UTF-8 encoding, the
119
152
  * byte-count-preserving `analyzed` surface, and one {@link CharRun} per code
120
153
  * point locating it in all three spaces.
154
+ *
155
+ * An ALL-ASCII input takes the identity path. Nothing about the contract
156
+ * changes there — the three spaces coincide, which is what the
157
+ * byte-count-preserving rule above was chosen to guarantee — so the mapping is
158
+ * arithmetic and the per-character {@link CharRun} array is deferred to its
159
+ * first reader. The analyzer re-derived that map for every nested block body it
160
+ * descended into, and `pathresolve`'s own `locate` discards the coordinate
161
+ * facts of a body anyway (they are properties of the root input): on a 60 KiB
162
+ * document nested 64 deep it decoded 7.9 MiB and allocated a run object per
163
+ * byte of it, a quarter of total analysis time (#322).
121
164
  */
122
165
  export function analyzeCoordinates(original: string): CoordinateAnalysis {
166
+ if (!NON_ASCII.test(original)) return asciiCoordinates(original);
123
167
  const runs: CharRun[] = [];
124
168
  const u8: number[] = [];
125
169
  const analyzed: number[] = [];
@@ -175,9 +219,78 @@ export function analyzeCoordinates(original: string): CoordinateAnalysis {
175
219
  originalU8: Uint8Array.from(u8),
176
220
  runs,
177
221
  lineStarts,
222
+ ascii: false,
178
223
  };
179
224
  }
180
225
 
226
+ /**
227
+ * The identity analysis of an all-ASCII input.
228
+ *
229
+ * One byte per code unit, so `analyzed`, `originalU8` and `original` are the
230
+ * same sequence and every offset is the same number in all three spaces. Only
231
+ * `lineStarts` needs a scan; `runs` is built on demand by {@link asciiRuns}
232
+ * because most callers never ask for it.
233
+ */
234
+ function asciiCoordinates(original: string): CoordinateAnalysis {
235
+ const len = original.length;
236
+ const u8 = new Uint8Array(len);
237
+ const lineStarts = [0];
238
+ for (let i = 0; i < len; i++) {
239
+ const code = original.charCodeAt(i);
240
+ u8[i] = code;
241
+ if (code === 0x0a) lineStarts.push(i + 1);
242
+ }
243
+ let runs: CharRun[] | undefined;
244
+ return {
245
+ original,
246
+ // `analyzed` and `originalU8` are equal by definition here, but they stay
247
+ // separate arrays: they are separately typed, separately indexed surfaces
248
+ // and a shared buffer would let a write through one be read through the
249
+ // other.
250
+ analyzed: u8,
251
+ originalU8: u8.slice(),
252
+ get runs(): CharRun[] {
253
+ runs ??= asciiRuns(original, lineStarts);
254
+ return runs;
255
+ },
256
+ lineStarts,
257
+ ascii: true,
258
+ };
259
+ }
260
+
261
+ /** The per-character runs of an all-ASCII input, built on first read. */
262
+ function asciiRuns(original: string, lineStarts: readonly number[]): CharRun[] {
263
+ const runs: CharRun[] = new Array(original.length);
264
+ let line = 0;
265
+ for (let i = 0; i < original.length; i++) {
266
+ const codePoint = original.charCodeAt(i);
267
+ runs[i] = {
268
+ codePoint,
269
+ utf16Start: i,
270
+ utf16Len: 1,
271
+ byteStart: i,
272
+ byteLen: 1,
273
+ ascii: true,
274
+ line,
275
+ col: i - (lineStarts[line] as number),
276
+ };
277
+ if (codePoint === 0x0a) line++;
278
+ }
279
+ return runs;
280
+ }
281
+
282
+ /** 0-based line containing `byte` — the last line start at or before it. */
283
+ function lineAtByte(lineStarts: readonly number[], byte: number): number {
284
+ let lo = 0;
285
+ let hi = lineStarts.length - 1;
286
+ while (lo < hi) {
287
+ const mid = (lo + hi + 1) >> 1;
288
+ if ((lineStarts[mid] as number) <= byte) lo = mid;
289
+ else hi = mid - 1;
290
+ }
291
+ return lo;
292
+ }
293
+
181
294
  /**
182
295
  * The {@link CharRun} whose byte range contains `byte`. Throws if `byte` is not
183
296
  * inside any run (i.e. `byte < 0` or `byte >= analyzed.length`).
@@ -188,6 +301,21 @@ export function runAtByte(a: CoordinateAnalysis, byte: number): CharRun {
188
301
  throw new Error(
189
302
  `runAtByte: ${byte} is not a valid interior byte offset; expected an integer in [0, ${len})`,
190
303
  );
304
+ // On the identity path the run IS the byte, so answer it rather than build
305
+ // (and then binary-search) an array of one object per character.
306
+ if (a.ascii) {
307
+ const line = lineAtByte(a.lineStarts, byte);
308
+ return {
309
+ codePoint: a.original.charCodeAt(byte),
310
+ utf16Start: byte,
311
+ utf16Len: 1,
312
+ byteStart: byte,
313
+ byteLen: 1,
314
+ ascii: true,
315
+ line,
316
+ col: byte - (a.lineStarts[line] as number),
317
+ };
318
+ }
191
319
  let lo = 0;
192
320
  let hi = a.runs.length - 1;
193
321
  while (lo <= hi) {
@@ -214,6 +342,12 @@ export function byteToPosition(a: CoordinateAnalysis, byte: number): Position {
214
342
  `byteToPosition: ${byte} is not a valid cursor offset; expected an integer in [0, ${len}]`,
215
343
  );
216
344
  if (byte === len) {
345
+ // The identity path reads the same answer off `lineStarts`: a trailing
346
+ // newline has already pushed the line that the cursor sits at column 0 of.
347
+ if (a.ascii) {
348
+ const line = a.lineStarts.length - 1;
349
+ return { line, col: len - (a.lineStarts[line] as number) };
350
+ }
217
351
  const last = a.runs.at(-1);
218
352
  if (!last) return { line: 0, col: 0 };
219
353
  if (last.codePoint === 0x0a) return { line: last.line + 1, col: 0 };
@@ -264,6 +398,9 @@ export function byteSpanToRange(
264
398
  * as part of its `non-ascii` run like any other character.
265
399
  */
266
400
  export function coordinateDefects(a: CoordinateAnalysis): Defect[] {
401
+ // Both classes are non-ASCII code points by definition (U+FEFF included), so
402
+ // an all-ASCII input has neither and the runs never need building.
403
+ if (a.ascii) return [];
267
404
  const defects: Defect[] = [];
268
405
  const first = a.runs[0];
269
406
  const hasBom = first !== undefined && first.codePoint === BOM;
@@ -308,6 +445,28 @@ export function positionToByte(
308
445
  throw new Error(
309
446
  `positionToByte: (line ${line}, col ${col}) is not a valid position; expected non-negative integers`,
310
447
  );
448
+ // On the identity path a position is arithmetic: the column IS the byte
449
+ // offset from the line start.
450
+ //
451
+ // The bound is the byte one PAST the line's last character, and on a line
452
+ // that ends in a newline that last character is the newline — so the bound
453
+ // is the next line's start, not one before it. That is the boundary the scan
454
+ // below reaches through `lastOnLine`, whose last run on the line is the
455
+ // newline itself; getting it wrong refused (line 0, col 2) of `a\n\nb`,
456
+ // which the scan answers with byte 2.
457
+ if (a.ascii) {
458
+ if (line >= a.lineStarts.length)
459
+ throw new Error(
460
+ `positionToByte: no character boundary at (line ${line}, col ${col})`,
461
+ );
462
+ const start = a.lineStarts[line] as number;
463
+ const end = (a.lineStarts[line + 1] ?? a.original.length) as number;
464
+ if (start + col > end)
465
+ throw new Error(
466
+ `positionToByte: no character boundary at (line ${line}, col ${col})`,
467
+ );
468
+ return start + col;
469
+ }
311
470
  // Single pass: return an exact (line, col) hit immediately; otherwise keep
312
471
  // the last run seen on the target line to resolve a just-past-the-end column.
313
472
  let lastOnLine: CharRun | undefined;