@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.23

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 (88) hide show
  1. package/CHANGELOG.md +177 -40
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +12 -8
  4. package/SECURITY.md +11 -3
  5. package/commands/api/README.md +39 -15
  6. package/commands/api/examples.md +44 -6
  7. package/commands/execute/README.md +108 -14
  8. package/commands/execute/examples.md +137 -3
  9. package/commands/explain/README.md +430 -129
  10. package/commands/explain/examples.md +260 -1
  11. package/commands/mcp/README.md +11 -0
  12. package/commands/mcp/examples.md +6 -0
  13. package/commands/retrieve/README.md +30 -13
  14. package/commands/retrieve/examples.md +10 -6
  15. package/commands/transfer/examples.md +9 -0
  16. package/docs/CLI.md +8 -8
  17. package/docs/CONSTITUTION.md +29 -2
  18. package/docs/MATRIX.md +26 -13
  19. package/docs/errors/cdb/empty-group.md +19 -3
  20. package/docs/errors/cdb/empty-selection.md +9 -3
  21. package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
  22. package/docs/errors/cmr/empty-selection.md +17 -3
  23. package/docs/errors/cmr/rule-selects-every-device.md +32 -0
  24. package/docs/errors/cmr/selection-includes-controller.md +27 -0
  25. package/docs/errors/usage/flag-after-separator.md +34 -0
  26. package/package.json +10 -5
  27. package/src/api-fanout.ts +11 -1
  28. package/src/api.ts +205 -65
  29. package/src/btest.ts +7 -2
  30. package/src/cli/api.ts +30 -16
  31. package/src/cli/common.ts +180 -1
  32. package/src/cli/devices.ts +0 -2
  33. package/src/cli/discover.ts +0 -1
  34. package/src/cli/execute.ts +44 -20
  35. package/src/cli/explain.ts +2 -0
  36. package/src/cli/mcp.ts +0 -1
  37. package/src/cli/retrieve.ts +40 -18
  38. package/src/cli/settings.ts +0 -3
  39. package/src/cli/terminal.ts +0 -2
  40. package/src/cli/transfer.ts +10 -8
  41. package/src/cli/version.ts +87 -0
  42. package/src/cli.ts +5 -3
  43. package/src/core/cmr-fleet.ts +486 -0
  44. package/src/core/cmr.ts +78 -0
  45. package/src/core/envelope.ts +32 -3
  46. package/src/core/error-catalog.ts +25 -3
  47. package/src/core/execute-command.ts +144 -19
  48. package/src/core/fanout.ts +130 -6
  49. package/src/errors.ts +2 -1
  50. package/src/execute-cmr.ts +95 -78
  51. package/src/execute-fanout.ts +11 -1
  52. package/src/execute.ts +105 -23
  53. package/src/explain/args.ts +69 -8
  54. package/src/explain/blocks.ts +3 -4
  55. package/src/explain/brace-slots.ts +5 -0
  56. package/src/explain/catalog.ts +13 -6
  57. package/src/explain/defects.ts +1 -0
  58. package/src/explain/group-faults.ts +927 -0
  59. package/src/explain/highlight-projection.ts +10 -8
  60. package/src/explain/operators.ts +20 -19
  61. package/src/explain/pathresolve.ts +102 -20
  62. package/src/explain/quoted-string.ts +41 -6
  63. package/src/explain/run-tokens.ts +223 -0
  64. package/src/explain/scope-brace.ts +103 -6
  65. package/src/explain/segment.ts +41 -3
  66. package/src/explain/spaced-args.ts +116 -0
  67. package/src/explain/symbols.ts +17 -1
  68. package/src/explain/transport.ts +13 -0
  69. package/src/explain/verbsplit.ts +14 -188
  70. package/src/explain/write.ts +8 -1
  71. package/src/explain.ts +378 -18
  72. package/src/index.ts +18 -1
  73. package/src/mcp/safety.ts +11 -0
  74. package/src/mcp/tools.ts +4 -1
  75. package/src/offline-gate.ts +30 -9
  76. package/src/protocols/btest-session.ts +30 -10
  77. package/src/protocols/mac-telnet-console.ts +16 -0
  78. package/src/resolver/cdb.ts +13 -1
  79. package/src/resolver/index.ts +4 -0
  80. package/src/resolver/selection.ts +37 -1
  81. package/src/resolver/target.ts +96 -8
  82. package/src/retrieve-fanout.ts +4 -1
  83. package/src/retrieve-follow.ts +29 -33
  84. package/src/retrieve-sample.ts +14 -8
  85. package/src/retrieve-stream.ts +38 -6
  86. package/src/retrieve.ts +73 -59
  87. package/src/transfer-fanout.ts +8 -1
  88. package/src/transfer.ts +15 -19
@@ -87,7 +87,7 @@ import {
87
87
  } from "./segment.ts";
88
88
 
89
89
  /** A RouterOS argument name: bare word, optionally dotted (`.id`, `.proplist`). */
90
- const ARGUMENT_NAME = /^\.?[A-Za-z][A-Za-z0-9._-]*$/;
90
+ export const ARGUMENT_NAME = /^\.?[A-Za-z][A-Za-z0-9._-]*$/;
91
91
 
92
92
  /** What one argument token is. */
93
93
  export type ArgumentKind = "attribute" | "positional" | "query";
@@ -327,6 +327,12 @@ interface WalkOptions {
327
327
  * see {@link braceSlotTakesArray}.
328
328
  */
329
329
  directiveVerb?: string;
330
+ /**
331
+ * Where a command's top-level `where` positional ends, set by the walk itself
332
+ * once it reads one. From there on a brace is an expression value
333
+ * (see {@link comparesNamedValue}).
334
+ */
335
+ whereAt?: number;
330
336
  }
331
337
 
332
338
  /** How deep member scanning descends before it stops and keeps only the shape. */
@@ -363,7 +369,11 @@ function braceOpensArray(
363
369
  options: WalkOptions,
364
370
  ): boolean {
365
371
  const verb = options.directiveVerb;
366
- if (verb === undefined) return false;
372
+ if (verb === undefined)
373
+ return (
374
+ options.whereAt !== undefined &&
375
+ !comparesNamedValue(text, at, options.whereAt)
376
+ );
367
377
  const owner = braceValueOwner(text, tokenStart, at);
368
378
  if (owner === undefined) return false;
369
379
  return braceSlotTakesArray(
@@ -390,6 +400,7 @@ function* walkArguments(
390
400
  let i = Math.max(0, from);
391
401
  // How many positionals have been read, which is the NEXT positional's index.
392
402
  let positionals = 0;
403
+ let walk = options;
393
404
  while (i < text.length) {
394
405
  // The MASKED view decides whitespace, because `maskComments` blanks a
395
406
  // continuation comment that `text` still spells with a `#`. Reading `text`
@@ -412,7 +423,7 @@ function* walkArguments(
412
423
  i += continuation;
413
424
  continue;
414
425
  }
415
- const token = scanToken(text, structural, i, positionals, options);
426
+ const token = scanToken(text, structural, i, positionals, walk);
416
427
  if (typeof token === "string") {
417
428
  yield token;
418
429
  return;
@@ -430,7 +441,7 @@ function* walkArguments(
430
441
  // the refusal exists to prevent. Shape only, never a value.
431
442
  const read =
432
443
  token.why === undefined
433
- ? readToken(text, structural, i, token.end, options)
444
+ ? readToken(text, structural, i, token.end, walk)
434
445
  : undecidedToken(text, i, token.end, token.why);
435
446
  if (typeof read === "string") {
436
447
  if (options.tolerant !== true) {
@@ -446,6 +457,13 @@ function* walkArguments(
446
457
  continue;
447
458
  }
448
459
  if (read.kind === "positional") positionals++;
460
+ if (
461
+ read.kind === "positional" &&
462
+ read.text === "where" &&
463
+ walk.directiveVerb === undefined &&
464
+ walk.whereAt === undefined
465
+ )
466
+ walk = { ...walk, whereAt: token.end };
449
467
  yield read;
450
468
  i = token.end;
451
469
  }
@@ -1661,13 +1679,51 @@ export function lexValueAnchors(
1661
1679
  return { complete: true, anchors };
1662
1680
  }
1663
1681
 
1682
+ /**
1683
+ * Whether the brace at `at` is the right side of a `where` comparison that
1684
+ * RouterOS reads with the named property's own value parser (#502).
1685
+ *
1686
+ * A `where` clause is expression context, so a brace there is an array value —
1687
+ * except straight after `<name>=`, `<name>!=` or `<name>==`, spaced or not.
1688
+ * On CHR 7.24.5 and 7.23.8, `:parse` rejects `/ip/route/print where
1689
+ * comment={1;2}` with `expected comment value`, and `where disabled={…}` with
1690
+ * `expected yes or no`. It accepts the brace after `in`, `~`, `<`, `>`, `<=`
1691
+ * and `>=`, on the left of `=`, and after `!interface=`, `$x=` and `"a"=`.
1692
+ *
1693
+ * Only a property name the menu KNOWS is read this way: `where foo-bar={…}` is
1694
+ * accepted. Offline has no per-menu property list (decision 3), so every bare
1695
+ * name counts, and an unknown one is a pinned over-strict reading.
1696
+ *
1697
+ * The name may open a group, `where (interface={"x"})` and `or(comment={…})`,
1698
+ * which `group-faults.ts` asks about (#509).
1699
+ */
1700
+ export function comparesNamedValue(
1701
+ text: string,
1702
+ at: number,
1703
+ floor: number,
1704
+ ): boolean {
1705
+ let j = at - 1;
1706
+ while (j > floor && /\s/.test(text[j] as string)) j--;
1707
+ if (text[j] !== "=") return false;
1708
+ while (j > floor && text[j] === "=") j--;
1709
+ if (text[j] === "<" || text[j] === ">") return false;
1710
+ if (text[j] === "!") j--;
1711
+ while (j > floor && /\s/.test(text[j] as string)) j--;
1712
+ const nameEnd = j + 1;
1713
+ while (j > floor && /[A-Za-z0-9._-]/.test(text[j] as string)) j--;
1714
+ const name = text.slice(j + 1, nameEnd);
1715
+ return /^[A-Za-z.]/.test(name) && /[\s(]/.test(text[j] as string);
1716
+ }
1717
+
1664
1718
  /**
1665
1719
  * Locate top-level `{...}` values that RouterOS rejects in command arguments.
1666
1720
  *
1667
1721
  * Parentheses and substitutions restore expression context, where a brace
1668
- * literal is legal. Stored-script attributes are also excluded: their brace is
1669
- * a script body, not an array. The caller applies this only to non-root
1670
- * commands; root scripting directives use the `(verb, slot)` table instead.
1722
+ * literal is legal, and so does a `where` clause, apart from the comparisons
1723
+ * {@link comparesNamedValue} names. Stored-script attributes are also
1724
+ * excluded: their brace is a script body, not an array. The caller applies
1725
+ * this only to non-root commands; root scripting directives use the
1726
+ * `(verb, slot)` table instead.
1671
1727
  */
1672
1728
  export function invalidCommandBraceOffsets(
1673
1729
  text: string,
@@ -1681,6 +1737,7 @@ export function invalidCommandBraceOffsets(
1681
1737
  let i = Math.max(0, from);
1682
1738
  let tokenStart: number | null = null;
1683
1739
  let positionals = 0;
1740
+ let whereAt: number | null = null;
1684
1741
  while (i < structural.length) {
1685
1742
  const c = structural[i] as string;
1686
1743
  if (
@@ -1689,6 +1746,7 @@ export function invalidCommandBraceOffsets(
1689
1746
  ) {
1690
1747
  if (tokenStart !== null) {
1691
1748
  const token = structural.slice(tokenStart, i);
1749
+ if (whereAt === null && token === "where") whereAt = i;
1692
1750
  if (
1693
1751
  !token.startsWith("?") &&
1694
1752
  unquotedEquals(token, 0, token.length) === null
@@ -1737,7 +1795,10 @@ export function invalidCommandBraceOffsets(
1737
1795
  : braceStartsStatements(range.masked, range.start + i, range.start)) ||
1738
1796
  (directiveVerb !== undefined &&
1739
1797
  slot !== undefined &&
1740
- braceSlotAcceptsBrace(directiveVerb, slot))
1798
+ braceSlotAcceptsBrace(directiveVerb, slot)) ||
1799
+ (directiveVerb === undefined &&
1800
+ whereAt !== null &&
1801
+ !comparesNamedValue(structural, i, whereAt))
1741
1802
  ) {
1742
1803
  i = end ?? structural.length;
1743
1804
  continue;
@@ -13,10 +13,9 @@
13
13
  * name (and, for the one head-dependent case, the leading directive) off the
14
14
  * statement itself.
15
15
  *
16
- * This module ships only the (single-pass, non-recursive) classification
17
- * primitives the path resolver needs. The recursive block *tree* / topology
18
- * surface stays in the lab until it can be promoted with a bounded, index-based
19
- * traversal of its own.
16
+ * This module ships the single-pass, non-recursive classification primitives
17
+ * the path resolver needs. `explain.ts` publishes source ownership from that
18
+ * resolver's existing bounded traversal; `scopeBlocks` remains a shallow scan.
20
19
  *
21
20
  * Every string skip here goes through `segment.ts`'s `scanQuotedString` rather
22
21
  * than a local quote-to-quote loop, so this module and the segmenter cannot
@@ -134,6 +134,11 @@ export function braceSlotTakesArray(verb: string, slot: string): boolean {
134
134
  return BRACE_ARRAY_SLOTS.get(verb)?.has(slot) === true;
135
135
  }
136
136
 
137
+ /** Whether a `{…}` in this root-directive slot is code or script text. */
138
+ export function braceSlotTakesProgram(verb: string, slot: string): boolean {
139
+ return BRACE_PROGRAM_SLOTS.get(verb)?.has(slot) === true;
140
+ }
141
+
137
142
  /** Whether the device sweep proved this root-directive slot accepts `{...}`. */
138
143
  export function braceSlotAcceptsBrace(verb: string, slot: string): boolean {
139
144
  return (
@@ -1317,6 +1317,11 @@ export function lookupPath(
1317
1317
  return PATH_CATALOG.get(`/${segments.join("/").toLowerCase()}`);
1318
1318
  }
1319
1319
 
1320
+ /** Segments in the deepest catalog path; no lookup past it can match. */
1321
+ const MAX_PATH_DEPTH = Math.max(
1322
+ ...Array.from(PATH_CATALOG.keys(), (path) => path.split("/").length - 1),
1323
+ );
1324
+
1320
1325
  /**
1321
1326
  * Where a published COMMAND ends inside a path run: the index of the run
1322
1327
  * segment that is the VERB, or `null` when no prefix of `segments` is one.
@@ -1340,12 +1345,14 @@ export function lookupPath(
1340
1345
  * question; what the segment IS is not.
1341
1346
  */
1342
1347
  export function commandVerbIndex(segments: readonly string[]): number | null {
1343
- for (let depth = 1; depth <= segments.length; depth++)
1344
- if (
1345
- PATH_CATALOG.get(`/${segments.slice(0, depth).join("/").toLowerCase()}`)
1346
- ?.kind === "command"
1347
- )
1348
- return depth - 1;
1348
+ // Bounded by the deepest row and built incrementally: a run of thousands of
1349
+ // bare words would otherwise re-join every prefix (#496).
1350
+ const limit = Math.min(segments.length, MAX_PATH_DEPTH);
1351
+ let path = "";
1352
+ for (let depth = 0; depth < limit; depth++) {
1353
+ path += `/${(segments[depth] as string).toLowerCase()}`;
1354
+ if (PATH_CATALOG.get(path)?.kind === "command") return depth;
1355
+ }
1349
1356
  return null;
1350
1357
  }
1351
1358
 
@@ -99,6 +99,7 @@ export type DefectCode =
99
99
  | "over-depth"
100
100
  | "bad-escape"
101
101
  | "bad-string-escape"
102
+ | "bad-string-variable"
102
103
  | "bad-sigil"
103
104
  | "invalid-hash"
104
105
  | "invalid-apostrophe"