@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.24
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 +106 -0
- package/GLOSSARY.txt +14 -0
- package/README.md +2 -2
- package/commands/api/AGENTS.md +18 -1
- package/commands/api/README.md +10 -6
- package/commands/api/examples.md +1 -1
- package/commands/execute/README.md +26 -6
- package/commands/execute/examples.md +24 -5
- package/commands/explain/README.md +452 -61
- package/commands/explain/examples.md +191 -41
- package/commands/retrieve/README.md +13 -2
- package/commands/retrieve/examples.md +30 -1
- package/commands/transfer/README.md +8 -4
- package/docs/CLI.md +19 -5
- package/docs/CONSTITUTION.md +14 -9
- package/docs/MATRIX.md +34 -21
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +2 -2
- package/src/api.ts +34 -39
- package/src/cli/explain.ts +206 -47
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +15 -0
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/routeros-errors.ts +25 -0
- package/src/execute-cmr.ts +2 -8
- package/src/execute.ts +48 -20
- package/src/explain/args.ts +115 -10
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +136 -1
- package/src/explain/symbols.ts +146 -6
- package/src/explain/values.ts +55 -4
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +395 -43
- package/src/index.ts +6 -0
- package/src/protocols/adapter.ts +88 -10
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +77 -27
- package/src/transfer.ts +38 -13
package/src/explain.ts
CHANGED
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
* separate optional homes but no offline producer. Each fact has its own
|
|
71
71
|
* provenance; no hint validates a value or becomes a diagnostic. The
|
|
72
72
|
* decision and the probe matrix are #225; `op` is deliberately not a shape
|
|
73
|
-
* hint — `(> …)` abstains offline
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* hint — `(> …)` abstains offline AND live, because identical syntax can
|
|
74
|
+
* be `op` or `array` and only `:typeof`, which live explain never sends,
|
|
75
|
+
* distinguishes them (#288, #225). This module is deliberately
|
|
76
76
|
* type-blind, which is why a verdict that looks wrong around a value is a
|
|
77
77
|
* type-axis question before it is a lexical one.
|
|
78
78
|
* - **`runtimeAcceptance` is always `"not-proven"`**, offline and live alike.
|
|
@@ -101,7 +101,12 @@ import type {
|
|
|
101
101
|
CommonSettingsMeta,
|
|
102
102
|
EnvelopeMeta,
|
|
103
103
|
} from "./core/envelope.ts";
|
|
104
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
buildTip,
|
|
106
|
+
type EnvelopeValidationMeta,
|
|
107
|
+
type Tip,
|
|
108
|
+
type Warning,
|
|
109
|
+
} from "./core/envelope.ts";
|
|
105
110
|
import {
|
|
106
111
|
type CanonicalExecuteCommand,
|
|
107
112
|
canonicalizeExecuteCommand,
|
|
@@ -172,7 +177,12 @@ import {
|
|
|
172
177
|
type ExplainTransport,
|
|
173
178
|
} from "./explain/transport.ts";
|
|
174
179
|
import { valueSpans } from "./explain/value-tokens.ts";
|
|
175
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
type ObservedValueType,
|
|
182
|
+
type SchemaValueType,
|
|
183
|
+
type ValueShape,
|
|
184
|
+
valueShapeHints,
|
|
185
|
+
} from "./explain/values.ts";
|
|
176
186
|
import {
|
|
177
187
|
type DocumentVerbSplit,
|
|
178
188
|
resolveVerb,
|
|
@@ -460,15 +470,21 @@ export interface ExplainStructure {
|
|
|
460
470
|
*
|
|
461
471
|
* The vocabulary is centrs-owned (the ratified decision): consumers depend on
|
|
462
472
|
* it rather than on raw RouterOS `highlight` classes, so cross-version drift in
|
|
463
|
-
* those classes is centrs's problem to absorb.
|
|
464
|
-
*
|
|
473
|
+
* those classes is centrs's problem to absorb. Name-coincident classes keep
|
|
474
|
+
* their spelling; live classes outside this vocabulary remain unclassified
|
|
475
|
+
* here and are preserved in the raw highlight observation.
|
|
465
476
|
*/
|
|
466
477
|
export type ExplainSpanClass =
|
|
467
478
|
| "comment"
|
|
468
479
|
| "variable-local"
|
|
469
480
|
| "variable-global"
|
|
470
481
|
| "variable-auto"
|
|
471
|
-
| "variable-parameter"
|
|
482
|
+
| "variable-parameter"
|
|
483
|
+
| "dir"
|
|
484
|
+
| "cmd"
|
|
485
|
+
| "arg"
|
|
486
|
+
| "escaped"
|
|
487
|
+
| "unclassified";
|
|
472
488
|
|
|
473
489
|
export interface ExplainSpan extends ExplainSpanRange {
|
|
474
490
|
class: ExplainSpanClass;
|
|
@@ -620,8 +636,34 @@ export interface ExplainValueShapeFact {
|
|
|
620
636
|
ev: string;
|
|
621
637
|
}
|
|
622
638
|
|
|
639
|
+
/**
|
|
640
|
+
* A type the device's parser visibly converted a bare literal to (#225).
|
|
641
|
+
*
|
|
642
|
+
* Only a conversion is evidence: `:parse` prints a string unquoted, so an
|
|
643
|
+
* unchanged rendering (`123`, `1.1.1.1`, `"2.2"`) proves nothing and the fact
|
|
644
|
+
* is absent. See `src/explain-live-types.ts`.
|
|
645
|
+
*/
|
|
623
646
|
export interface ExplainValueTypeFact {
|
|
624
|
-
value:
|
|
647
|
+
value: ObservedValueType;
|
|
648
|
+
/** The device's own rendering of the literal (`2.2` → `2.0.0.2`). */
|
|
649
|
+
rendered: string;
|
|
650
|
+
ev: string;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* An argument's declared type, normalized from a schema source (#225).
|
|
655
|
+
*
|
|
656
|
+
* `values` are the leaf types the declaration mentions, not alternatives of
|
|
657
|
+
* one value: `gateway` (`Address[%Interface][@VRF]`) lists `ip`, `ip6` and
|
|
658
|
+
* `enum`, because inspect prints the interface names like keywords. `declared` is the source's own top-level term, verbatim,
|
|
659
|
+
* so a consumer can tell a live `/console/inspect` declaration from a CLI
|
|
660
|
+
* Reference `Type` cell by `ev` and still read the original.
|
|
661
|
+
*/
|
|
662
|
+
export interface ExplainSchemaTypeFact {
|
|
663
|
+
values: SchemaValueType[];
|
|
664
|
+
/** Present only when the declaration spells a repetition; absence is not a claim. */
|
|
665
|
+
list?: true;
|
|
666
|
+
declared: string;
|
|
625
667
|
ev: string;
|
|
626
668
|
}
|
|
627
669
|
|
|
@@ -629,7 +671,7 @@ export interface ExplainValueTypeFact {
|
|
|
629
671
|
export interface ExplainValueFacts {
|
|
630
672
|
shapeHints?: ExplainValueShapeFact;
|
|
631
673
|
observedType?: ExplainValueTypeFact;
|
|
632
|
-
schemaType?:
|
|
674
|
+
schemaType?: ExplainSchemaTypeFact;
|
|
633
675
|
}
|
|
634
676
|
|
|
635
677
|
export interface ExplainValueOccurrence {
|
|
@@ -670,18 +712,14 @@ export interface ExplainDiagnostic {
|
|
|
670
712
|
}
|
|
671
713
|
|
|
672
714
|
/**
|
|
673
|
-
* Where
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* probe runs and there is no device to stamp. The type says only what offline
|
|
678
|
-
* emits rather than pre-declaring variants with no producer, so a consumer
|
|
679
|
-
* cannot narrow on a `source` that never appears; phase 2 widens the union and
|
|
680
|
-
* adds the stamp, which is an ADDITION to the shape a caller reads today.
|
|
715
|
+
* Where an analysis fact came from.
|
|
716
|
+
*
|
|
717
|
+
* Live probes add version, observation time, input identity and coverage. Their
|
|
718
|
+
* outcome describes acquisition, never runtime acceptance.
|
|
681
719
|
*/
|
|
682
720
|
export interface ExplainEvidence {
|
|
683
721
|
id: string;
|
|
684
|
-
source: "canonicalizer";
|
|
722
|
+
source: "canonicalizer" | "live-inspect";
|
|
685
723
|
/** The analyzer that produced the facts citing this entry. */
|
|
686
724
|
probe: string;
|
|
687
725
|
/**
|
|
@@ -692,7 +730,157 @@ export interface ExplainEvidence {
|
|
|
692
730
|
* drawn from live responses.
|
|
693
731
|
*/
|
|
694
732
|
basis: "direct" | "heuristic" | "derived";
|
|
695
|
-
outcome:
|
|
733
|
+
outcome:
|
|
734
|
+
| "ok"
|
|
735
|
+
| "empty"
|
|
736
|
+
| "timeout"
|
|
737
|
+
| "transport-error"
|
|
738
|
+
| "auth-error"
|
|
739
|
+
| "unsupported"
|
|
740
|
+
| "skipped"
|
|
741
|
+
| "interrupted"
|
|
742
|
+
| "invalid-response";
|
|
743
|
+
/** Live observation provenance; absent on canonicalizer evidence. */
|
|
744
|
+
version?: string;
|
|
745
|
+
observedAt?: string;
|
|
746
|
+
statementIds?: string[];
|
|
747
|
+
path?: string;
|
|
748
|
+
coverage?: ExplainSpanRange;
|
|
749
|
+
inputSha256?: string;
|
|
750
|
+
normalization?: "original-utf8" | "sub-per-non-ascii-byte";
|
|
751
|
+
reason?: string;
|
|
752
|
+
errorCode?: CentrsError["code"];
|
|
753
|
+
/**
|
|
754
|
+
* The facet a live probe served; an `empty` facet answer is not a warning.
|
|
755
|
+
* `types` is the value-type probes (#225), which run on every live call.
|
|
756
|
+
*/
|
|
757
|
+
facet?: "schema" | "complete" | "types";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** One `request=syntax` row, verbatim: the device's declared help for one argument. */
|
|
761
|
+
export interface ExplainSyntaxRow {
|
|
762
|
+
symbol: string;
|
|
763
|
+
/** RouterOS `symbol-type` (`definition`, `explanation`, …). */
|
|
764
|
+
kind: string;
|
|
765
|
+
text: string;
|
|
766
|
+
/** RouterOS `nested` depth. */
|
|
767
|
+
depth: number;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** A menu or verb under a described menu, with counts when it was expanded. */
|
|
771
|
+
export interface ExplainSchemaChild {
|
|
772
|
+
name: string;
|
|
773
|
+
verbs?: number;
|
|
774
|
+
menus?: number;
|
|
775
|
+
arguments?: number;
|
|
776
|
+
ev?: string;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export interface ExplainSchemaArgument {
|
|
780
|
+
name: string;
|
|
781
|
+
/** Argument-level `syntax` rows, present when within the probe budget. */
|
|
782
|
+
syntax?: ExplainSyntaxRow[];
|
|
783
|
+
ev?: string;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* `--schema`: what the device declares at one path (#492). `child` decides
|
|
788
|
+
* the node; a menu lists verbs and submenus, a command its arguments, `print`
|
|
789
|
+
* also its `.proplist`. Declared facts only — never print rows or state.
|
|
790
|
+
*/
|
|
791
|
+
export interface ExplainSchema {
|
|
792
|
+
/** The `child` probe on the described path. */
|
|
793
|
+
ev: string;
|
|
794
|
+
path: string;
|
|
795
|
+
statementId?: string;
|
|
796
|
+
node: "menu" | "command" | "argument" | "unknown";
|
|
797
|
+
verbs?: ExplainSchemaChild[];
|
|
798
|
+
menus?: ExplainSchemaChild[];
|
|
799
|
+
arguments?: ExplainSchemaArgument[];
|
|
800
|
+
/** Output-field names a `print` can show; not print rows. */
|
|
801
|
+
proplist?: {
|
|
802
|
+
ev: string;
|
|
803
|
+
source: "print-proplist" | "get-value-name";
|
|
804
|
+
names: string[];
|
|
805
|
+
};
|
|
806
|
+
/** Argument node only: its `syntax` rows. */
|
|
807
|
+
syntax?: ExplainSyntaxRow[];
|
|
808
|
+
/** Full sizes as the device reported them, before any list was cut. */
|
|
809
|
+
counts: {
|
|
810
|
+
verbs?: number;
|
|
811
|
+
menus?: number;
|
|
812
|
+
arguments?: number;
|
|
813
|
+
proplist?: number;
|
|
814
|
+
};
|
|
815
|
+
/** A list was cut or children were left unexpanded. */
|
|
816
|
+
truncated: boolean;
|
|
817
|
+
/** Narrower inputs that expand what this result left out. */
|
|
818
|
+
next: string[];
|
|
819
|
+
reason?: string;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export interface ExplainCompletionCandidate {
|
|
823
|
+
text: string;
|
|
824
|
+
/** `variable`: a name in a `where` clause, which RouterOS scopes as a variable. */
|
|
825
|
+
kind: "menu" | "verb" | "argument" | "variable" | "value";
|
|
826
|
+
/** `exact` when the typed prefix already spells the whole candidate. */
|
|
827
|
+
match: "partial" | "exact";
|
|
828
|
+
/** The input-completion probe that offered it. */
|
|
829
|
+
ev: string;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* `--complete`: what may follow at the cursor (#492). Every candidate is an
|
|
834
|
+
* observation: the device's input completion, cited by `ev`. It is never a
|
|
835
|
+
* closed set, and a value may reflect configuration state (interface names).
|
|
836
|
+
* Declared facts (`child`, argument `syntax`) live in {@link ExplainSchema}.
|
|
837
|
+
*/
|
|
838
|
+
export interface ExplainCompletion {
|
|
839
|
+
/** Analyzed byte offset; equals the device's UTF-8 byte offset. */
|
|
840
|
+
cursor: number;
|
|
841
|
+
/** The cursor in the original document: 0-based line, UTF-16 character. */
|
|
842
|
+
at: { line: number; character: number };
|
|
843
|
+
statementId?: string;
|
|
844
|
+
context: "path" | "argument-name" | "variable" | "value" | "unsupported";
|
|
845
|
+
/** Bytes a chosen candidate replaces; `end` is the cursor. */
|
|
846
|
+
replace: ExplainSpanRange;
|
|
847
|
+
prefix: string;
|
|
848
|
+
/** The command at the cursor, when known (argument-name and value contexts). */
|
|
849
|
+
path?: string;
|
|
850
|
+
/** Value context: the argument (or `where` property) whose value is being completed. */
|
|
851
|
+
argument?: string;
|
|
852
|
+
/** Exact argument name: whether the device offers `=` at the cursor. */
|
|
853
|
+
takesValue?: boolean;
|
|
854
|
+
candidates: ExplainCompletionCandidate[];
|
|
855
|
+
/** Argument value context: argument-level `syntax` rows (type information). */
|
|
856
|
+
syntax?: ExplainSyntaxRow[];
|
|
857
|
+
count: number;
|
|
858
|
+
truncated: boolean;
|
|
859
|
+
reason?: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export interface ExplainLiveData {
|
|
863
|
+
environment?: {
|
|
864
|
+
ev: string;
|
|
865
|
+
version: string;
|
|
866
|
+
architecture?: string;
|
|
867
|
+
board?: string;
|
|
868
|
+
};
|
|
869
|
+
/** Exact parse output; absent when empty, skipped or unavailable. */
|
|
870
|
+
parse?: {
|
|
871
|
+
ev: string;
|
|
872
|
+
output: string;
|
|
873
|
+
rejected: boolean;
|
|
874
|
+
position?: { line: number; column: number };
|
|
875
|
+
};
|
|
876
|
+
/** Raw device classes, retained separately from the centrs span vocabulary. */
|
|
877
|
+
highlight?: { ev: string; classes: string[] };
|
|
878
|
+
arguments: {
|
|
879
|
+
ev: string;
|
|
880
|
+
path: string;
|
|
881
|
+
statementIds: string[];
|
|
882
|
+
names: string[];
|
|
883
|
+
}[];
|
|
696
884
|
}
|
|
697
885
|
|
|
698
886
|
export interface ExplainData {
|
|
@@ -703,6 +891,12 @@ export interface ExplainData {
|
|
|
703
891
|
symbols: ExplainSymbols;
|
|
704
892
|
values: ExplainValues;
|
|
705
893
|
spans: ExplainSpan[];
|
|
894
|
+
/** Read-only device observations, present only on live calls. */
|
|
895
|
+
live?: ExplainLiveData;
|
|
896
|
+
/** Live `--schema` only. */
|
|
897
|
+
schema?: ExplainSchema;
|
|
898
|
+
/** Live `--complete` only. */
|
|
899
|
+
completion?: ExplainCompletion;
|
|
706
900
|
/**
|
|
707
901
|
* Total, gapless token partition of `[0, input.bytes)`.
|
|
708
902
|
*
|
|
@@ -725,8 +919,8 @@ export interface ExplainData {
|
|
|
725
919
|
|
|
726
920
|
export interface ExplainOperationMeta {
|
|
727
921
|
command: "explain";
|
|
728
|
-
/**
|
|
729
|
-
mode: "offline";
|
|
922
|
+
/** Whether a target was consulted for read-only evidence. */
|
|
923
|
+
mode: "offline" | "live";
|
|
730
924
|
statementCount: number;
|
|
731
925
|
verdict: ExplainVerdict;
|
|
732
926
|
}
|
|
@@ -1271,11 +1465,24 @@ export function buildTokens(
|
|
|
1271
1465
|
return out;
|
|
1272
1466
|
}
|
|
1273
1467
|
|
|
1468
|
+
/** Source order, with the most severe finding first at the same byte. */
|
|
1469
|
+
export function compareExplainDiagnostics(
|
|
1470
|
+
a: ExplainDiagnostic,
|
|
1471
|
+
b: ExplainDiagnostic,
|
|
1472
|
+
): number {
|
|
1473
|
+
return (
|
|
1474
|
+
a.span.start - b.span.start ||
|
|
1475
|
+
severityRank(b.severity) - severityRank(a.severity)
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1274
1479
|
function severityRank(s: ExplainSeverity): number {
|
|
1275
1480
|
return s === "error" ? 2 : s === "warning" ? 1 : 0;
|
|
1276
1481
|
}
|
|
1277
1482
|
|
|
1278
|
-
function verdictOf(
|
|
1483
|
+
export function verdictOf(
|
|
1484
|
+
diagnostics: readonly ExplainDiagnostic[],
|
|
1485
|
+
): ExplainVerdict {
|
|
1279
1486
|
let worst = 0;
|
|
1280
1487
|
for (const d of diagnostics)
|
|
1281
1488
|
worst = Math.max(worst, severityRank(d.severity));
|
|
@@ -1479,7 +1686,11 @@ export function explainCommand(
|
|
|
1479
1686
|
}),
|
|
1480
1687
|
...braceDiagnostics,
|
|
1481
1688
|
...groupFaultDiagnostics(
|
|
1482
|
-
findGroupFaults(
|
|
1689
|
+
findGroupFaults(
|
|
1690
|
+
analyzed,
|
|
1691
|
+
analyzedRange.masked,
|
|
1692
|
+
defects.find(({ defect }) => defect.code === "bom")?.defect.end,
|
|
1693
|
+
),
|
|
1483
1694
|
braceDiagnostics,
|
|
1484
1695
|
),
|
|
1485
1696
|
...separators.flatMap((runs) => runs.map(missingSeparatorDiagnostic)),
|
|
@@ -1487,11 +1698,7 @@ export function explainCommand(
|
|
|
1487
1698
|
pairs.filter((pair) => pair.valueShaped).map(spacedArgumentDiagnostic),
|
|
1488
1699
|
),
|
|
1489
1700
|
...statements.flatMap((s) => diagnosticsForStatement(s)),
|
|
1490
|
-
].sort(
|
|
1491
|
-
(a, b) =>
|
|
1492
|
-
a.span.start - b.span.start ||
|
|
1493
|
-
severityRank(b.severity) - severityRank(a.severity),
|
|
1494
|
-
);
|
|
1701
|
+
].sort(compareExplainDiagnostics);
|
|
1495
1702
|
|
|
1496
1703
|
const valueFacts = valuesOf(
|
|
1497
1704
|
verbs.splits,
|
|
@@ -2134,6 +2341,18 @@ function spacedArgumentDiagnostic(pair: SpacedPair): ExplainDiagnostic {
|
|
|
2134
2341
|
const INVALID_COMMAND_BRACE =
|
|
2135
2342
|
"brace arrays are not valid in command arguments — use a parenthesized comma list such as `(1,2)`, or a bare comma list where the argument schema accepts one";
|
|
2136
2343
|
|
|
2344
|
+
/**
|
|
2345
|
+
* A `name={` with a script-like body that is not a proven script slot (#525).
|
|
2346
|
+
*
|
|
2347
|
+
* `warning`, not `error`: `down={ :beep }` lowers to `down-script=` on CHR
|
|
2348
|
+
* 7.24.5 and 7.23.8, while `comment={ :beep }` is a `syntax error` there, and
|
|
2349
|
+
* telling them apart needs the command's argument list, which is schema. The
|
|
2350
|
+
* brace is read as a body; the device decides the slot.
|
|
2351
|
+
*/
|
|
2352
|
+
function possibleScriptBodyMessage(name: string): string {
|
|
2353
|
+
return `possible script-body abbreviation: \`${name}=\` with a \`{…}\` body may be a script-valued argument or abbreviate one; offline has no per-command argument list to confirm the slot, so the brace is read as a script body — device validation decides`;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2137
2356
|
/**
|
|
2138
2357
|
* The group faults as diagnostics (#505, #509, #511, #512).
|
|
2139
2358
|
*
|
|
@@ -2198,9 +2417,35 @@ function groupFaultMessage(f: GroupFault): string {
|
|
|
2198
2417
|
return '`$` must be followed by a variable name (letters and digits) or `"name"`, and RouterOS rejects it here (`syntax error`) — quote a literal dollar (`"\\$"`)';
|
|
2199
2418
|
case "invalid-name":
|
|
2200
2419
|
return 'a variable name is letters and digits only, and RouterOS rejects any other byte in it (`expected end of command`) — quote the name (`:local "a_b"`) to use other characters';
|
|
2420
|
+
case "unquoted-non-ascii":
|
|
2421
|
+
case "unquoted-reserved-byte":
|
|
2422
|
+
return unquotedByteMessage(f);
|
|
2201
2423
|
}
|
|
2202
2424
|
}
|
|
2203
2425
|
|
|
2426
|
+
/**
|
|
2427
|
+
* The unquoted-byte faults (#358, #371). The remedy follows the site: a value
|
|
2428
|
+
* can be quoted, an argument name must be corrected, and a path, command or
|
|
2429
|
+
* `$name` must lose the byte (a variable name may be quoted, `$"café"`).
|
|
2430
|
+
*/
|
|
2431
|
+
function unquotedByteMessage(f: GroupFault): string {
|
|
2432
|
+
const what =
|
|
2433
|
+
f.code === "unquoted-non-ascii"
|
|
2434
|
+
? "a non-ASCII character outside a string"
|
|
2435
|
+
: f.detail === "="
|
|
2436
|
+
? "a second `=` in a bare argument"
|
|
2437
|
+
: `an unquoted \`${f.detail}\``;
|
|
2438
|
+
if (f.site === "argument-name")
|
|
2439
|
+
return `${what} in an argument name: RouterOS rejects the name at its first byte (\`expected end of command\`) — correct the argument name; no RouterOS argument name holds this byte`;
|
|
2440
|
+
if (f.site === "head-word")
|
|
2441
|
+
return `${what} in a path or command word: RouterOS rejects it (\`expected command name\` or \`expected end of command\`) — remove or retype the byte (a \`?\` is the console's help key, and a pasted non-breaking space or curly quote only looks like ASCII); a variable name may be quoted (\`$"café"\`)`;
|
|
2442
|
+
if (f.code === "unquoted-non-ascii")
|
|
2443
|
+
return `${what}: RouterOS reads only ASCII in a bare word, name or value, and rejects it (\`expected end of command\`) — quote it (\`name="café"\`, \`$"café"\`), or retype a pasted non-breaking space, curly quote or ellipsis`;
|
|
2444
|
+
return f.detail === "="
|
|
2445
|
+
? `${what} value: RouterOS rejects it (\`expected end of command\`) — quote the value (\`comment="a=b"\`), as a base64 key ending in \`=\` needs`
|
|
2446
|
+
: `${what} is never part of a bare word, and RouterOS rejects it (\`expected end of command\`) — quote the value (\`url="http://x/a?b"\`)`;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2204
2449
|
function invalidCommandBraceDiagnostics(
|
|
2205
2450
|
splits: readonly DocumentVerbSplit[],
|
|
2206
2451
|
analyzed: string,
|
|
@@ -2212,12 +2457,15 @@ function invalidCommandBraceDiagnostics(
|
|
|
2212
2457
|
const { start, end } = split.span;
|
|
2213
2458
|
const text = analyzed.slice(start, end);
|
|
2214
2459
|
if (text !== split.text) continue;
|
|
2215
|
-
// The
|
|
2216
|
-
//
|
|
2217
|
-
//
|
|
2218
|
-
//
|
|
2219
|
-
//
|
|
2220
|
-
//
|
|
2460
|
+
// The segmenter flattens a relative submenu block only where the source
|
|
2461
|
+
// alone names the menu (#351, `menuBlockPath`): not after a navigation
|
|
2462
|
+
// statement (`/ip⏎address { … }`), and not for an abbreviated or
|
|
2463
|
+
// digit-led menu word. Those still reach here as one split. Do not
|
|
2464
|
+
// reinterpret a known submenu's leading `{` as an argument value, but
|
|
2465
|
+
// retain diagnostics for rejected braces later in the same split.
|
|
2466
|
+
// Require evidence that the brace actually opens a menu container, so
|
|
2467
|
+
// `/ip/route/print {1;2}` and `/ip { firewall/filter/print {1;2} }` still
|
|
2468
|
+
// diagnose the `{1;2}` array.
|
|
2221
2469
|
//
|
|
2222
2470
|
// A verb the vocabulary or the catalog names is no menu, so its leading
|
|
2223
2471
|
// brace is a positional operand: `/system/script add { :beep }` and
|
|
@@ -2284,18 +2532,28 @@ function invalidCommandBraceDiagnostics(
|
|
|
2284
2532
|
}
|
|
2285
2533
|
}
|
|
2286
2534
|
}
|
|
2287
|
-
for (const
|
|
2535
|
+
for (const finding of invalidCommandBraceOffsets(
|
|
2288
2536
|
text,
|
|
2289
2537
|
split.argsAt,
|
|
2290
2538
|
split.path === "/" ? split.verb : undefined,
|
|
2291
2539
|
nestedRange(analyzedRange, start, end),
|
|
2292
2540
|
)) {
|
|
2293
|
-
if (at === menuBraceAt) continue;
|
|
2541
|
+
if (finding.at === menuBraceAt) continue;
|
|
2542
|
+
if (finding.ambiguous) {
|
|
2543
|
+
out.push({
|
|
2544
|
+
code: "explain/canonicalizer/possible-script-body-abbreviation",
|
|
2545
|
+
severity: "warning",
|
|
2546
|
+
message: possibleScriptBodyMessage(finding.name ?? "name"),
|
|
2547
|
+
span: { start: start + finding.at, end: start + finding.at + 1 },
|
|
2548
|
+
ev: EV.values,
|
|
2549
|
+
});
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2294
2552
|
out.push({
|
|
2295
2553
|
code: "explain/canonicalizer/invalid-command-brace",
|
|
2296
2554
|
severity: "error",
|
|
2297
2555
|
message: INVALID_COMMAND_BRACE,
|
|
2298
|
-
span: { start: start + at, end: start + at + 1 },
|
|
2556
|
+
span: { start: start + finding.at, end: start + finding.at + 1 },
|
|
2299
2557
|
ev: EV.values,
|
|
2300
2558
|
});
|
|
2301
2559
|
}
|
|
@@ -2611,7 +2869,7 @@ export function explainEnvelope(
|
|
|
2611
2869
|
buildTip(
|
|
2612
2870
|
"tip/explain-offline-only",
|
|
2613
2871
|
"No router was given, so this is the canonicalizer's reading alone.",
|
|
2614
|
-
"
|
|
2872
|
+
"Add a router (`centrs explain <router> '<input>'`) for read-only parse, highlight and argument-name evidence; `--schema` and `--complete` then describe paths and complete at the cursor.",
|
|
2615
2873
|
),
|
|
2616
2874
|
...(options.tips ?? []),
|
|
2617
2875
|
];
|
|
@@ -2620,7 +2878,7 @@ export function explainEnvelope(
|
|
|
2620
2878
|
target: {},
|
|
2621
2879
|
via: null,
|
|
2622
2880
|
settings: settingsMeta(options.format),
|
|
2623
|
-
validation:
|
|
2881
|
+
validation: explainValidationMeta(),
|
|
2624
2882
|
operation: {
|
|
2625
2883
|
command: "explain",
|
|
2626
2884
|
mode: "offline",
|
|
@@ -2645,7 +2903,7 @@ export function buildExplainFacetTip(facets: readonly string[]): Tip {
|
|
|
2645
2903
|
return buildTip(
|
|
2646
2904
|
"tip/explain-live-facets",
|
|
2647
2905
|
`${named} enumerate what a device accepts, and no router was given — so nothing is enumerated here.`,
|
|
2648
|
-
"Name a router
|
|
2906
|
+
"Name a router (`centrs explain <router> '<input>' --schema`) to ask the device; use `rosetta` for documented RouterOS schema facts.",
|
|
2649
2907
|
);
|
|
2650
2908
|
}
|
|
2651
2909
|
|
|
@@ -2779,10 +3037,23 @@ export function buildExplainErrorEnvelope(
|
|
|
2779
3037
|
error: serializeCentrsError(centrs),
|
|
2780
3038
|
warnings: [],
|
|
2781
3039
|
tips,
|
|
2782
|
-
meta: {
|
|
3040
|
+
meta: {
|
|
3041
|
+
target: {},
|
|
3042
|
+
via: null,
|
|
3043
|
+
settings: settingsMeta(format),
|
|
3044
|
+
validation: explainValidationMeta(),
|
|
3045
|
+
},
|
|
2783
3046
|
};
|
|
2784
3047
|
}
|
|
2785
3048
|
|
|
3049
|
+
/**
|
|
3050
|
+
* explain is not a gate: it never runs the input, so there is no gate verdict
|
|
3051
|
+
* to report, offline or live. Its verdict is `data.verdict` (#535).
|
|
3052
|
+
*/
|
|
3053
|
+
export function explainValidationMeta(): EnvelopeValidationMeta {
|
|
3054
|
+
return { enabled: false };
|
|
3055
|
+
}
|
|
3056
|
+
|
|
2786
3057
|
export function renderExplainEnvelope(
|
|
2787
3058
|
envelope: ExplainEnvelope | ExplainErrorEnvelope,
|
|
2788
3059
|
format: ExplainOutputFormat,
|
|
@@ -2844,7 +3115,85 @@ function renderValue(occurrence: ExplainValueOccurrence): string {
|
|
|
2844
3115
|
const shapes = occurrence.facts.shapeHints?.values.join("|") ?? "unknown";
|
|
2845
3116
|
const parent =
|
|
2846
3117
|
occurrence.parent === undefined ? "" : ` in=${occurrence.parent}`;
|
|
2847
|
-
|
|
3118
|
+
const observed = occurrence.facts.observedType;
|
|
3119
|
+
const schema = occurrence.facts.schemaType;
|
|
3120
|
+
const live = `${observed ? ` observed=${observed.value}(${observed.rendered})` : ""}${schema ? ` schema=${schema.values.join("|")}${schema.list ? "[list]" : ""}` : ""}`;
|
|
3121
|
+
return `${occurrence.kind.padEnd(10)}${name}${parent} shapes=${shapes}${live}`;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function renderLiveSummary(live: ExplainLiveData): string {
|
|
3125
|
+
const device = live.environment
|
|
3126
|
+
? `${live.environment.version}${live.environment.architecture ? ` ${live.environment.architecture}` : ""}`
|
|
3127
|
+
: "device unavailable";
|
|
3128
|
+
const parse = live.parse
|
|
3129
|
+
? `parse ${live.parse.rejected ? "rejected" : "accepted"}${live.parse.position ? ` at ${live.parse.position.line}:${live.parse.position.column}` : ""}`
|
|
3130
|
+
: "parse unavailable";
|
|
3131
|
+
const highlight = live.highlight
|
|
3132
|
+
? `highlight ${live.highlight.classes.length} byte(s)`
|
|
3133
|
+
: "highlight unavailable";
|
|
3134
|
+
const names = live.arguments.reduce(
|
|
3135
|
+
(total, entry) => total + entry.names.length,
|
|
3136
|
+
0,
|
|
3137
|
+
);
|
|
3138
|
+
return `live: ${device} · ${parse} · ${highlight} · arguments ${live.arguments.length} path(s), ${names} name(s)`;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
/** Names only, at most a dozen per line; `--json` keeps every fact. */
|
|
3142
|
+
function renderSchemaSummary(schema: ExplainSchema): string[] {
|
|
3143
|
+
const list = (label: string, names: readonly string[], total?: number) =>
|
|
3144
|
+
` ${label} (${total ?? names.length}): ${names.slice(0, 12).join(", ")}${(total ?? names.length) > Math.min(names.length, 12) ? ", …" : ""}`;
|
|
3145
|
+
const lines = [
|
|
3146
|
+
`schema: ${schema.path} is ${schema.node === "unknown" ? "unknown to the device" : `a ${schema.node}`}${schema.truncated ? " (truncated)" : ""}${schema.reason ? ` — ${schema.reason}` : ""}`,
|
|
3147
|
+
];
|
|
3148
|
+
if (schema.verbs)
|
|
3149
|
+
lines.push(
|
|
3150
|
+
list(
|
|
3151
|
+
"verbs",
|
|
3152
|
+
schema.verbs.map((v) => v.name),
|
|
3153
|
+
schema.counts.verbs,
|
|
3154
|
+
),
|
|
3155
|
+
);
|
|
3156
|
+
if (schema.menus?.length)
|
|
3157
|
+
lines.push(
|
|
3158
|
+
list(
|
|
3159
|
+
"menus",
|
|
3160
|
+
schema.menus.map((m) => m.name),
|
|
3161
|
+
schema.counts.menus,
|
|
3162
|
+
),
|
|
3163
|
+
);
|
|
3164
|
+
if (schema.arguments)
|
|
3165
|
+
lines.push(
|
|
3166
|
+
list(
|
|
3167
|
+
"arguments",
|
|
3168
|
+
schema.arguments.map((a) => a.name),
|
|
3169
|
+
schema.counts.arguments,
|
|
3170
|
+
),
|
|
3171
|
+
);
|
|
3172
|
+
if (schema.proplist)
|
|
3173
|
+
lines.push(list("proplist", schema.proplist.names, schema.counts.proplist));
|
|
3174
|
+
if (schema.syntax)
|
|
3175
|
+
for (const row of schema.syntax)
|
|
3176
|
+
lines.push(` ${row.symbol ? `${row.symbol}: ` : ""}${row.text}`);
|
|
3177
|
+
return lines;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function renderCompletionSummary(completion: ExplainCompletion): string {
|
|
3181
|
+
const head = `complete @${completion.cursor}: ${completion.context}${completion.argument ? ` ${completion.argument}=` : ""}${completion.prefix ? ` "${completion.prefix}"` : ""}`;
|
|
3182
|
+
if (completion.reason && completion.candidates.length === 0)
|
|
3183
|
+
return `${head} — ${completion.reason}`;
|
|
3184
|
+
const shown = completion.candidates
|
|
3185
|
+
.slice(0, 12)
|
|
3186
|
+
.map((c) => (c.match === "exact" ? `${c.text}*` : c.text));
|
|
3187
|
+
const value =
|
|
3188
|
+
completion.takesValue === undefined
|
|
3189
|
+
? ""
|
|
3190
|
+
: completion.takesValue
|
|
3191
|
+
? " (takes a value: `=`)"
|
|
3192
|
+
: " (flag, no value)";
|
|
3193
|
+
const syntax = completion.syntax?.length
|
|
3194
|
+
? ` [${completion.syntax.map((row) => row.text || row.symbol).join("; ")}]`
|
|
3195
|
+
: "";
|
|
3196
|
+
return `${head} → ${completion.count === 0 ? "no candidates" : `${shown.join(", ")}${completion.count > shown.length ? ", …" : ""}`}${value}${syntax}`;
|
|
2848
3197
|
}
|
|
2849
3198
|
|
|
2850
3199
|
/**
|
|
@@ -2874,6 +3223,9 @@ function renderExplainText(
|
|
|
2874
3223
|
`gate: ${canonical.mode}${canonical.path ? ` ${canonical.path}` : ""}${canonical.verb ? ` ${canonical.verb}` : ""} · ${argCount} arg(s)${canonical.writeShaped ? " · write-shaped" : ""}`,
|
|
2875
3224
|
);
|
|
2876
3225
|
lines.push(`write: ${structure.containsWrite}`);
|
|
3226
|
+
if (data.live) lines.push(renderLiveSummary(data.live));
|
|
3227
|
+
if (data.schema) lines.push(...renderSchemaSummary(data.schema));
|
|
3228
|
+
if (data.completion) lines.push(renderCompletionSummary(data.completion));
|
|
2877
3229
|
if (data.input.normalized)
|
|
2878
3230
|
lines.push(
|
|
2879
3231
|
`input: ${data.input.bytes} byte(s), normalized (non-ASCII stood in for one-for-one)`,
|
package/src/index.ts
CHANGED
|
@@ -480,6 +480,7 @@ export {
|
|
|
480
480
|
type ExplainEvidence,
|
|
481
481
|
type ExplainFailOn,
|
|
482
482
|
type ExplainInput,
|
|
483
|
+
type ExplainLiveData,
|
|
483
484
|
type ExplainMenuReading,
|
|
484
485
|
type ExplainOperationMeta,
|
|
485
486
|
type ExplainOutputFormat,
|
|
@@ -514,6 +515,11 @@ export {
|
|
|
514
515
|
explainOutputFormats,
|
|
515
516
|
renderExplainEnvelope,
|
|
516
517
|
} from "./explain.ts";
|
|
518
|
+
export {
|
|
519
|
+
EXPLAIN_LIVE_LIMITS,
|
|
520
|
+
type ExplainLiveRequest,
|
|
521
|
+
explainLiveEnvelope,
|
|
522
|
+
} from "./explain-live.ts";
|
|
517
523
|
export {
|
|
518
524
|
assertOfflineSyntax,
|
|
519
525
|
isOfflineGateRejection,
|