@tikoci/centrs 0.1.3 → 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 +348 -0
- package/GLOSSARY.txt +252 -0
- package/LICENSE +21 -0
- package/README.md +44 -12
- package/commands/AGENTS.md +14 -5
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +56 -26
- package/commands/api/examples.md +46 -1
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +6 -1
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +77 -36
- package/commands/execute/examples.md +120 -10
- package/commands/explain/README.md +2756 -0
- package/commands/explain/examples.md +702 -0
- package/commands/mcp/README.md +7 -4
- package/commands/mcp/examples.md +2 -2
- package/commands/retrieve/README.md +25 -27
- package/commands/retrieve/examples.md +16 -0
- package/commands/terminal/examples.md +7 -1
- package/commands/transfer/README.md +28 -20
- package/commands/transfer/examples.md +29 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +138 -30
- package/docs/MATRIX.md +92 -18
- package/docs/errors/README.md +3 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/syntax.md +29 -4
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +56 -10
- package/src/api-fanout.ts +46 -18
- package/src/api.ts +351 -121
- package/src/cli/api.ts +26 -7
- package/src/cli/btest.ts +19 -3
- package/src/cli/execute.ts +25 -6
- package/src/cli/explain.ts +503 -0
- package/src/cli/retrieve.ts +33 -1
- package/src/cli/selection.ts +71 -11
- package/src/cli/settings.ts +2 -2
- package/src/cli/terminal.ts +1 -1
- package/src/cli/transfer.ts +23 -6
- package/src/cli.ts +15 -2
- package/src/core/envelope.ts +58 -2
- package/src/core/error-catalog.ts +42 -1
- package/src/core/execute-command.ts +148 -0
- package/src/core/inspect.ts +36 -4
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +47 -18
- package/src/execute.ts +339 -307
- package/src/explain/arg-tokens.ts +151 -0
- package/src/explain/args.ts +1749 -0
- package/src/explain/blocks.ts +175 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +39 -0
- package/src/explain/catalog.ts +1343 -0
- package/src/explain/coordinates.ts +488 -0
- package/src/explain/defects.ts +218 -0
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +668 -0
- package/src/explain/operator-tokens.ts +316 -0
- package/src/explain/operators.ts +465 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1569 -0
- package/src/explain/quoted-string.ts +331 -0
- package/src/explain/scope-brace.ts +461 -0
- package/src/explain/segment.ts +1165 -0
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +77 -0
- package/src/explain/symbol-values.ts +556 -0
- package/src/explain/symbols.ts +1617 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +514 -0
- package/src/explain/value-tokens.ts +83 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +955 -0
- package/src/explain/write.ts +856 -0
- package/src/explain.ts +2602 -0
- package/src/index.ts +225 -6
- package/src/mcp/server.ts +2 -1
- package/src/offline-gate.ts +148 -0
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/facts.ts +1 -1
- package/src/resolver/index.ts +23 -4
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +99 -12
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/target.ts +1 -1
- package/src/retrieve-fanout.ts +27 -13
- package/src/retrieve.ts +82 -77
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +43 -26
- package/src/transfer.ts +88 -45
package/src/index.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export const centrsVersion = "0.1.0";
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
2
|
type ApiEnvelope,
|
|
5
3
|
type ApiErrorEnvelope,
|
|
@@ -81,6 +79,7 @@ export {
|
|
|
81
79
|
type EnvelopeMeta,
|
|
82
80
|
type EnvelopeTargetMeta,
|
|
83
81
|
type EnvelopeValidationMeta,
|
|
82
|
+
type EnvelopeValidationStage,
|
|
84
83
|
type FanoutData,
|
|
85
84
|
type FanoutSummary,
|
|
86
85
|
normalizeWarnings,
|
|
@@ -110,6 +109,7 @@ export {
|
|
|
110
109
|
type InspectChildItem,
|
|
111
110
|
type InspectCompletionItem,
|
|
112
111
|
type InspectRequestKind,
|
|
112
|
+
inspectArgumentNames,
|
|
113
113
|
inspectChildren,
|
|
114
114
|
inspectChildrenOrEmpty,
|
|
115
115
|
inspectCompletions,
|
|
@@ -118,6 +118,25 @@ export {
|
|
|
118
118
|
isCommandNode,
|
|
119
119
|
pathTokens,
|
|
120
120
|
} from "./core/inspect.ts";
|
|
121
|
+
export {
|
|
122
|
+
assertCapability,
|
|
123
|
+
type CapabilityRequirement,
|
|
124
|
+
checkCapability,
|
|
125
|
+
compareRouterOsVersion,
|
|
126
|
+
type DeviceVersion,
|
|
127
|
+
type DeviceVersionSource,
|
|
128
|
+
type PreflightState,
|
|
129
|
+
type PreflightVerdict,
|
|
130
|
+
parseRouterOsVersion,
|
|
131
|
+
probeDeviceVersion,
|
|
132
|
+
type RouterOsVersionParts,
|
|
133
|
+
type RouterOsVersionRange,
|
|
134
|
+
renderSupportedRanges,
|
|
135
|
+
routerOsVersionAtLeast,
|
|
136
|
+
versionFactFromComment,
|
|
137
|
+
versionInRange,
|
|
138
|
+
} from "./core/preflight.ts";
|
|
139
|
+
export { centrsVersion } from "./core/version.ts";
|
|
121
140
|
export {
|
|
122
141
|
encodeMndpPacket,
|
|
123
142
|
MNDP_BROADCAST_ADDRESS,
|
|
@@ -276,6 +295,205 @@ export {
|
|
|
276
295
|
executeFanout,
|
|
277
296
|
renderExecuteFanoutEnvelope,
|
|
278
297
|
} from "./execute-fanout.ts";
|
|
298
|
+
// The lexer's own names are module-scoped and read naturally there; at the
|
|
299
|
+
// package root they need the `Explain` prefix every other explain export
|
|
300
|
+
// carries — a bare `Argument` says nothing about which of centrs's surfaces it
|
|
301
|
+
// belongs to. Aliasing at the re-export is the existing pattern here (see
|
|
302
|
+
// `occurrences as explainOccurrences` below).
|
|
303
|
+
//
|
|
304
|
+
// `ExplainArgument` and `ExplainArgumentToken` are the same SHAPE in two
|
|
305
|
+
// coordinate spaces: this one is what `lexArguments` returns, with spans
|
|
306
|
+
// relative to the statement it was handed; the `Token` is that rebased into
|
|
307
|
+
// document space by `explainCommand`. A consumer reading `explainCommand`'s
|
|
308
|
+
// result wants the latter.
|
|
309
|
+
//
|
|
310
|
+
// `lexExplainArgumentTokens` is the skip-tolerant reading (#316) and is
|
|
311
|
+
// exported for the same reason `lexExplainValueAnchors` is: a consumer building
|
|
312
|
+
// its own token-level rule needs the reading a rule wants, not the one a
|
|
313
|
+
// renderer wants. Its `undecided` tokens carry no `value` and never become one.
|
|
314
|
+
export {
|
|
315
|
+
type Argument as ExplainArgument,
|
|
316
|
+
type ArgumentKind as ExplainArgumentKind,
|
|
317
|
+
type ArgumentReading as ExplainArgumentReading,
|
|
318
|
+
type ArgumentsRead as ExplainArgumentsRead,
|
|
319
|
+
type ArgumentsUnread as ExplainArgumentsUnread,
|
|
320
|
+
type ArgumentTokenReading as ExplainArgumentTokenReading,
|
|
321
|
+
lexArguments as lexExplainArguments,
|
|
322
|
+
lexArgumentTokens as lexExplainArgumentTokens,
|
|
323
|
+
lexValueAnchors as lexExplainValueAnchors,
|
|
324
|
+
type ValueAnchor as ExplainValueAnchor,
|
|
325
|
+
type ValueAnchorKind as ExplainValueAnchorKind,
|
|
326
|
+
type ValueAnchorReading as ExplainValueAnchorReading,
|
|
327
|
+
} from "./explain/args.ts";
|
|
328
|
+
export {
|
|
329
|
+
DIRECTIVE_BODY,
|
|
330
|
+
HEAD_SCOPED_ARG_NAMES,
|
|
331
|
+
isScopeBrace,
|
|
332
|
+
matchBrace,
|
|
333
|
+
SCOPE_ARG_NAMES,
|
|
334
|
+
type ScopeBlock,
|
|
335
|
+
scopeBlocks,
|
|
336
|
+
scopeBodies,
|
|
337
|
+
scopeNameAt,
|
|
338
|
+
} from "./explain/blocks.ts";
|
|
339
|
+
export {
|
|
340
|
+
analyzeCoordinates,
|
|
341
|
+
byteSpanToRange,
|
|
342
|
+
byteToPosition,
|
|
343
|
+
type CharRun,
|
|
344
|
+
type CoordinateAnalysis,
|
|
345
|
+
coordinateDefects,
|
|
346
|
+
type Position,
|
|
347
|
+
positionToByte,
|
|
348
|
+
type Range,
|
|
349
|
+
runAtByte,
|
|
350
|
+
SUB,
|
|
351
|
+
} from "./explain/coordinates.ts";
|
|
352
|
+
export {
|
|
353
|
+
type Defect,
|
|
354
|
+
type DefectCode,
|
|
355
|
+
defectAt,
|
|
356
|
+
hasStructuralDefect,
|
|
357
|
+
isPositionalFact,
|
|
358
|
+
mergeDefects,
|
|
359
|
+
rebaseDefects,
|
|
360
|
+
} from "./explain/defects.ts";
|
|
361
|
+
export {
|
|
362
|
+
type DocumentAnalysis,
|
|
363
|
+
type Resolution,
|
|
364
|
+
resolveDocument,
|
|
365
|
+
resolveStatements,
|
|
366
|
+
type Span,
|
|
367
|
+
type StatementAnalysis,
|
|
368
|
+
type StatementResolution,
|
|
369
|
+
} from "./explain/pathresolve.ts";
|
|
370
|
+
export {
|
|
371
|
+
maskComments,
|
|
372
|
+
type QuotedStringScan,
|
|
373
|
+
type Segment,
|
|
374
|
+
type SegmentResult,
|
|
375
|
+
scanQuotedString,
|
|
376
|
+
segmentStatements,
|
|
377
|
+
} from "./explain/segment.ts";
|
|
378
|
+
export {
|
|
379
|
+
HIGHLIGHT_CLASS,
|
|
380
|
+
resolveSymbols,
|
|
381
|
+
type SymbolAnalysis,
|
|
382
|
+
type SymbolClass,
|
|
383
|
+
type SymbolOccurrence,
|
|
384
|
+
type SymbolRole,
|
|
385
|
+
} from "./explain/symbols.ts";
|
|
386
|
+
export type {
|
|
387
|
+
ExplainApiCandidateTransport,
|
|
388
|
+
ExplainExecuteTransport,
|
|
389
|
+
ExplainRestMethod,
|
|
390
|
+
ExplainRestRequest,
|
|
391
|
+
ExplainTransport,
|
|
392
|
+
ExplainTransportClassification,
|
|
393
|
+
ExplainUnknownTransport,
|
|
394
|
+
} from "./explain/transport.ts";
|
|
395
|
+
export {
|
|
396
|
+
VALUE_SHAPES,
|
|
397
|
+
type ValueContext as ExplainValueContext,
|
|
398
|
+
type ValueShape as ExplainValueShape,
|
|
399
|
+
type ValueShapeOptions as ExplainValueShapeOptions,
|
|
400
|
+
valueShapeHints as explainValueShapeHints,
|
|
401
|
+
} from "./explain/values.ts";
|
|
402
|
+
export {
|
|
403
|
+
type DocumentVerbSplit,
|
|
404
|
+
describeStatement,
|
|
405
|
+
isDirective,
|
|
406
|
+
type RunToken,
|
|
407
|
+
resolveVerb,
|
|
408
|
+
resolveVerbs,
|
|
409
|
+
runTokens,
|
|
410
|
+
type Sep,
|
|
411
|
+
type Split,
|
|
412
|
+
SUBMENU_DIRECTIVES,
|
|
413
|
+
splitRun,
|
|
414
|
+
VERBS,
|
|
415
|
+
type VerbAnalysis,
|
|
416
|
+
type VerbResolution,
|
|
417
|
+
type VerbSplit,
|
|
418
|
+
type VerbSplitCommandReading,
|
|
419
|
+
type VerbSplitMenuReading,
|
|
420
|
+
type VerbSplitRefusal,
|
|
421
|
+
} from "./explain/verbsplit.ts";
|
|
422
|
+
export {
|
|
423
|
+
classifyVerb,
|
|
424
|
+
containsWrite,
|
|
425
|
+
type Occurrence,
|
|
426
|
+
type OccurrenceClass,
|
|
427
|
+
// `occurrences` is too generic for the library root; the module-local name
|
|
428
|
+
// stays short because inside `write.ts` there is only one kind.
|
|
429
|
+
occurrences as explainOccurrences,
|
|
430
|
+
READ_VERBS,
|
|
431
|
+
ROOT_CMDS,
|
|
432
|
+
ROOT_DYNAMIC,
|
|
433
|
+
ROOT_WRITE,
|
|
434
|
+
WRITE_VERBS,
|
|
435
|
+
type WriteAnalysis,
|
|
436
|
+
type WriteVerdict,
|
|
437
|
+
} from "./explain/write.ts";
|
|
438
|
+
export {
|
|
439
|
+
buildExplainErrorEnvelope,
|
|
440
|
+
buildExplainFacetTip,
|
|
441
|
+
type ExplainArguments,
|
|
442
|
+
type ExplainArgumentToken,
|
|
443
|
+
type ExplainBlock,
|
|
444
|
+
type ExplainCanonical,
|
|
445
|
+
type ExplainCommandOptions,
|
|
446
|
+
type ExplainCommandReading,
|
|
447
|
+
type ExplainData,
|
|
448
|
+
type ExplainDiagnostic,
|
|
449
|
+
type ExplainEnvelope,
|
|
450
|
+
type ExplainEnvelopeOptions,
|
|
451
|
+
type ExplainErrorEnvelope,
|
|
452
|
+
type ExplainEvidence,
|
|
453
|
+
type ExplainFailOn,
|
|
454
|
+
type ExplainInput,
|
|
455
|
+
type ExplainMenuReading,
|
|
456
|
+
type ExplainOperationMeta,
|
|
457
|
+
type ExplainOutputFormat,
|
|
458
|
+
type ExplainPositionMapEntry,
|
|
459
|
+
type ExplainReading,
|
|
460
|
+
type ExplainRefusal,
|
|
461
|
+
type ExplainResolution,
|
|
462
|
+
type ExplainSeverity,
|
|
463
|
+
type ExplainSpan,
|
|
464
|
+
type ExplainSpanClass,
|
|
465
|
+
type ExplainSpanRange,
|
|
466
|
+
type ExplainStatement,
|
|
467
|
+
type ExplainStatementFacts,
|
|
468
|
+
type ExplainStructure,
|
|
469
|
+
type ExplainSubcommand,
|
|
470
|
+
type ExplainSubcommandFacts,
|
|
471
|
+
type ExplainSymbolClass,
|
|
472
|
+
type ExplainSymbolOccurrence,
|
|
473
|
+
type ExplainSymbols,
|
|
474
|
+
type ExplainToken,
|
|
475
|
+
type ExplainTokenClass,
|
|
476
|
+
type ExplainValueFacts,
|
|
477
|
+
type ExplainValueOccurrence,
|
|
478
|
+
type ExplainValueShapeFact,
|
|
479
|
+
type ExplainValues,
|
|
480
|
+
type ExplainValueTypeFact,
|
|
481
|
+
type ExplainVerdict,
|
|
482
|
+
explainCommand,
|
|
483
|
+
explainEnvelope,
|
|
484
|
+
explainExitCode,
|
|
485
|
+
explainFailOnLevels,
|
|
486
|
+
explainOutputFormats,
|
|
487
|
+
renderExplainEnvelope,
|
|
488
|
+
} from "./explain.ts";
|
|
489
|
+
export {
|
|
490
|
+
assertOfflineSyntax,
|
|
491
|
+
isOfflineGateRejection,
|
|
492
|
+
OFFLINE_GATE_SOURCE,
|
|
493
|
+
type OfflineGateFinding,
|
|
494
|
+
type OfflineGateResult,
|
|
495
|
+
type ValidationStageName,
|
|
496
|
+
} from "./offline-gate.ts";
|
|
279
497
|
export {
|
|
280
498
|
type ApiVerb,
|
|
281
499
|
createProtocolAdapter,
|
|
@@ -304,9 +522,6 @@ export {
|
|
|
304
522
|
type CdbGroupTarget,
|
|
305
523
|
type CdbResolution,
|
|
306
524
|
type CdbResolveInput,
|
|
307
|
-
type CdbSelectionExpansion,
|
|
308
|
-
type CdbSelectionMember,
|
|
309
|
-
type CdbSelectionResolveInput,
|
|
310
525
|
type CommentKvGeoKey,
|
|
311
526
|
type CommentKvKey,
|
|
312
527
|
type CommentKvLayer,
|
|
@@ -323,7 +538,7 @@ export {
|
|
|
323
538
|
defaultPortForScheme,
|
|
324
539
|
deviceLocation,
|
|
325
540
|
expandCdbGroup,
|
|
326
|
-
|
|
541
|
+
expandSelection,
|
|
327
542
|
isDefaultRecordTarget,
|
|
328
543
|
type NearPredicate,
|
|
329
544
|
type ParsedGpsTuple,
|
|
@@ -351,6 +566,9 @@ export {
|
|
|
351
566
|
resolveOptionalIntegerSetting,
|
|
352
567
|
resolveStringSetting,
|
|
353
568
|
resolveTarget,
|
|
569
|
+
type SelectionExpansion,
|
|
570
|
+
type SelectionMember,
|
|
571
|
+
type SelectionResolveInput,
|
|
354
572
|
type SelectionWhereClause,
|
|
355
573
|
type TargetResolveInput,
|
|
356
574
|
type TargetSelection,
|
|
@@ -470,6 +688,7 @@ export {
|
|
|
470
688
|
transferFanout,
|
|
471
689
|
} from "./transfer-fanout.ts";
|
|
472
690
|
|
|
691
|
+
import { centrsVersion } from "./core/version.ts";
|
|
473
692
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
474
693
|
|
|
475
694
|
export const plannedSurfaces = ["api", "cli", "tui", "mcp", "proxy"] as const;
|
package/src/mcp/server.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
13
|
+
import { centrsVersion } from "../core/version.ts";
|
|
13
14
|
import { type CentrsMcpConfig, resolveMcpConfig } from "./config.ts";
|
|
14
15
|
import {
|
|
15
16
|
buildDevicesResource,
|
|
@@ -79,7 +80,7 @@ export function createCentrsMcpServer(
|
|
|
79
80
|
config: CentrsMcpConfig = resolveMcpConfig(),
|
|
80
81
|
): McpServer {
|
|
81
82
|
const server = new McpServer(
|
|
82
|
-
{ name: "centrs-mcp", version:
|
|
83
|
+
{ name: "centrs-mcp", version: centrsVersion },
|
|
83
84
|
{ instructions: SERVER_INSTRUCTIONS },
|
|
84
85
|
);
|
|
85
86
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1 of the validation gate: the offline analyzer in front of the device
|
|
3
|
+
* preflight (GH#354).
|
|
4
|
+
*
|
|
5
|
+
* centrs used to run two validators that did not know about each other — the
|
|
6
|
+
* device preflight (`:put [:parse …]` plus `/console/inspect request=child`)
|
|
7
|
+
* that `execute` / `api` / `validate` share, and offline `explain`, which is
|
|
8
|
+
* byte-accurate and corpus-gated but took no part in validation at all. This
|
|
9
|
+
* module is the seam that joins them: every CLI-shaped input is analyzed here
|
|
10
|
+
* first, with no connection, and only then handed to the device stage.
|
|
11
|
+
*
|
|
12
|
+
* Three things that buys, in the order they matter:
|
|
13
|
+
*
|
|
14
|
+
* 1. A syntax fault is rejected with a **byte span** and **no round trip** — the
|
|
15
|
+
* protocol adapter is never even constructed.
|
|
16
|
+
* 2. The device stage narrows to the semantic half it is actually needed for
|
|
17
|
+
* (`:parse` is name-level only; `/console/inspect` owns the attribute set).
|
|
18
|
+
* 3. Where the device probe is unsafe or unavailable — the 7.12.2 class of
|
|
19
|
+
* problem in GH#343 — the gate degrades to a real gate instead of to nothing.
|
|
20
|
+
*
|
|
21
|
+
* **A clean offline pass is necessary, never sufficient.** `explain` publishes
|
|
22
|
+
* `runtimeAcceptance: "not-proven"` for a reason: `/console/inspect` accepts
|
|
23
|
+
* forms the runtime rejects, and the analyzer has no per-menu schema (decision 3
|
|
24
|
+
* — no offline schema snapshot). The device stage is not optional and this
|
|
25
|
+
* module never reports success on its behalf. `verdict: "warn"` is an
|
|
26
|
+
* ABSTENTION, not a rejection, and passes through for the same reason.
|
|
27
|
+
*
|
|
28
|
+
* `--validate=false` disables BOTH stages — one flag, one meaning. That was
|
|
29
|
+
* settled with the `--raw` precedence layer on GH#154 and is why callers gate
|
|
30
|
+
* this on the same resolved `validate` setting as the device stage; the escape
|
|
31
|
+
* hatch has to cover a stage-1 defect too (GH#355 is a live example of the
|
|
32
|
+
* analyzer being wrong in the other direction).
|
|
33
|
+
*
|
|
34
|
+
* The offending byte range rides `context.span`, never {@link CentrsError}'s
|
|
35
|
+
* `position`. `RouterOsErrorPosition` is documented as RouterOS's own
|
|
36
|
+
* authoritative 1-based byte column, "carried verbatim; never re-derived"
|
|
37
|
+
* (`errors.ts`) — synthesizing one from offline analysis would make a
|
|
38
|
+
* centrs-derived offset indistinguishable from a device-reported one.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { CentrsError } from "./errors.ts";
|
|
42
|
+
import { type ExplainDiagnostic, explainCommand } from "./explain.ts";
|
|
43
|
+
|
|
44
|
+
/** Validator identity for `meta.validation` and error context. */
|
|
45
|
+
export const OFFLINE_GATE_SOURCE = "offline explain (canonicalizer)";
|
|
46
|
+
|
|
47
|
+
/** Which stage of the two-stage gate produced a verdict. */
|
|
48
|
+
export type ValidationStageName = "offline" | "device";
|
|
49
|
+
|
|
50
|
+
/** One rejected span, flattened out of {@link ExplainDiagnostic} for the error context. */
|
|
51
|
+
export interface OfflineGateFinding {
|
|
52
|
+
/** The `explain/<analyzer>/<slug>` diagnostic code. */
|
|
53
|
+
code: string;
|
|
54
|
+
message: string;
|
|
55
|
+
/** Byte offsets into the analyzed input, end-exclusive. */
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Outcome of a stage-1 run that did not reject. */
|
|
61
|
+
export interface OfflineGateResult {
|
|
62
|
+
/**
|
|
63
|
+
* `pass` — the analyzer read the whole input. `warn` — it ABSTAINED on part
|
|
64
|
+
* of it (an unresolved menu, say). Both proceed to the device stage; only
|
|
65
|
+
* `fail` throws.
|
|
66
|
+
*/
|
|
67
|
+
verdict: "pass" | "warn";
|
|
68
|
+
/** Warning-severity diagnostics, kept so a caller can surface the abstention. */
|
|
69
|
+
warnings: readonly OfflineGateFinding[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findingOf(diagnostic: ExplainDiagnostic): OfflineGateFinding {
|
|
73
|
+
return {
|
|
74
|
+
code: diagnostic.code,
|
|
75
|
+
message: diagnostic.message,
|
|
76
|
+
start: diagnostic.span.start,
|
|
77
|
+
end: diagnostic.span.end,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run the offline stage over a RouterOS CLI string.
|
|
83
|
+
*
|
|
84
|
+
* Throws `validation/syntax` when the analyzer rejects the input; returns the
|
|
85
|
+
* verdict otherwise. Never opens a connection and never throws for any other
|
|
86
|
+
* reason — `explainCommand` itself is documented as total.
|
|
87
|
+
*
|
|
88
|
+
* `surface` names the caller in the error context (`execute`, `api /execute`) so
|
|
89
|
+
* a reader can tell which command's gate spoke; `via` is the resolved protocol
|
|
90
|
+
* when the caller has one, purely for context.
|
|
91
|
+
*/
|
|
92
|
+
export function assertOfflineSyntax(
|
|
93
|
+
command: string,
|
|
94
|
+
options: { surface: string; via?: string },
|
|
95
|
+
): OfflineGateResult {
|
|
96
|
+
const analysis = explainCommand(command);
|
|
97
|
+
const errors = analysis.diagnostics.filter(
|
|
98
|
+
(diagnostic) => diagnostic.severity === "error",
|
|
99
|
+
);
|
|
100
|
+
const warnings = analysis.diagnostics
|
|
101
|
+
.filter((diagnostic) => diagnostic.severity === "warning")
|
|
102
|
+
.map(findingOf);
|
|
103
|
+
|
|
104
|
+
if (analysis.verdict !== "fail") {
|
|
105
|
+
return { verdict: analysis.verdict === "warn" ? "warn" : "pass", warnings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A `fail` verdict always carries at least one error diagnostic, but read it
|
|
109
|
+
// defensively: an empty list would otherwise produce an error that says
|
|
110
|
+
// nothing about what was wrong, which is worse than a generic summary.
|
|
111
|
+
const findings = errors.map(findingOf);
|
|
112
|
+
const first = findings[0];
|
|
113
|
+
throw new CentrsError({
|
|
114
|
+
code: "validation/syntax",
|
|
115
|
+
summary: first
|
|
116
|
+
? `RouterOS syntax rejected by offline analysis: ${first.message}`
|
|
117
|
+
: "RouterOS syntax rejected by offline analysis.",
|
|
118
|
+
remediation:
|
|
119
|
+
"Run `centrs explain '<command>'` to see the offending bytes in context, " +
|
|
120
|
+
"fix the syntax, then retry. `--validate=false` bypasses both validation " +
|
|
121
|
+
"stages if you are deliberately probing a RouterOS edge centrs reads wrong.",
|
|
122
|
+
context: {
|
|
123
|
+
command,
|
|
124
|
+
validationStage: "offline" satisfies ValidationStageName,
|
|
125
|
+
validationSource: OFFLINE_GATE_SOURCE,
|
|
126
|
+
surface: options.surface,
|
|
127
|
+
...(options.via ? { via: options.via } : {}),
|
|
128
|
+
...(first ? { span: { start: first.start, end: first.end } } : {}),
|
|
129
|
+
diagnostics: findings,
|
|
130
|
+
},
|
|
131
|
+
causeData: first?.code ?? "offline analysis rejected the input",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* True when `error` is the rejection {@link assertOfflineSyntax} throws.
|
|
137
|
+
*
|
|
138
|
+
* Callers use this to build honest envelope metadata: on a stage-1 rejection the
|
|
139
|
+
* device stage did not run, and no connection was opened, so the meta must not
|
|
140
|
+
* claim `:parse` or `/console/inspect` failed.
|
|
141
|
+
*/
|
|
142
|
+
export function isOfflineGateRejection(error: unknown): boolean {
|
|
143
|
+
return (
|
|
144
|
+
error instanceof CentrsError &&
|
|
145
|
+
error.code === "validation/syntax" &&
|
|
146
|
+
(error.context?.["validationStage"] as string | undefined) === "offline"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
* scraped for: `(evl …)` = ok, `syntax error (line …)` = malformed, and
|
|
40
40
|
* `bad parameter <name> (line …)` = unknown attribute. A single console
|
|
41
41
|
* `:parse` therefore covers both the syntax and the unknown-attribute
|
|
42
|
-
* (
|
|
42
|
+
* (name-level) gate — no `/console/inspect` table parsing needed.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { parseRouterOsPosition } from "../core/routeros-errors.ts";
|
|
46
|
+
import { routerOsStringLiteral } from "../core/routeros-string.ts";
|
|
46
47
|
import { CentrsError } from "../errors.ts";
|
|
47
48
|
import {
|
|
48
49
|
type MacAddress,
|
|
@@ -323,8 +324,11 @@ export class MacTelnetConsole {
|
|
|
323
324
|
* Validate a command without running it, using a single console `:parse`.
|
|
324
325
|
* Throws `validation/syntax` for malformed CLI and
|
|
325
326
|
* `validation/unknown-attribute` for a rejected parameter; returns the parsed
|
|
326
|
-
* form on success. One gate covers both because the console
|
|
327
|
-
*
|
|
327
|
+
* form on success. One gate covers both because the console prints the
|
|
328
|
+
* `:parse` return value, which carries `bad parameter <name>` as well as the
|
|
329
|
+
* `(<%%` / `(line N column M)` syntax diagnostics — see
|
|
330
|
+
* {@link classifyParseResult} for the full grammar and why the matcher is
|
|
331
|
+
* anchored to the diagnostic shape rather than to bare words.
|
|
328
332
|
*/
|
|
329
333
|
async parseGate(cli: string): Promise<string> {
|
|
330
334
|
const { output } = await this.run(parseScriptFor(cli));
|
|
@@ -604,17 +608,61 @@ export function parseScriptFor(cli: string): string {
|
|
|
604
608
|
return `:put [:parse ${routerOsStringLiteral(cli)}]`;
|
|
605
609
|
}
|
|
606
610
|
|
|
611
|
+
/*
|
|
612
|
+
* The `:parse` diagnostic grammar, anchored to where RouterOS actually emits
|
|
613
|
+
* each shape. Anchoring is load-bearing, not cosmetic: an *accepted* return
|
|
614
|
+
* echoes the script body, so `:put "bad parameter x"` comes back as
|
|
615
|
+
* `(evl /putmessage=bad parameter x)`. An unanchored search reads that echoed
|
|
616
|
+
* content as a verdict and refuses to run a valid command (GH#230).
|
|
617
|
+
*
|
|
618
|
+
* Two shapes, both corpus-verified against lsp-routeros-ts
|
|
619
|
+
* `parseil_results.il_text` (2736 rows, RouterOS 7.20.8/7.22.1/7.23rc1):
|
|
620
|
+
*
|
|
621
|
+
* 1. WRAPPED — the diagnostic sits inside the returned value, marked by the
|
|
622
|
+
* `(evl ` / `(<%%` wrapper. It can appear mid-return, because a
|
|
623
|
+
* multi-statement script reports the offending statement in place. Every
|
|
624
|
+
* corpus `bad parameter` (8/8) follows `(evl `, and every `bad command
|
|
625
|
+
* name` (135/135) follows `(<%%`.
|
|
626
|
+
* 2. BARE — the diagnostic *is* the whole return, printed on its own line
|
|
627
|
+
* with RouterOS's position suffix. All 425 corpus rows of this shape are
|
|
628
|
+
* the entire return; none appear embedded. Requiring both line-start and
|
|
629
|
+
* the `(line N column M)` suffix is what separates a real verdict from
|
|
630
|
+
* echoed body text.
|
|
631
|
+
*/
|
|
632
|
+
const WRAPPED_BAD_PARAMETER = /\(evl\s+bad parameter\s+(\S+)/i;
|
|
633
|
+
const WRAPPED_BAD_COMMAND = /\(<%%\s*bad command name\b/i;
|
|
634
|
+
const BARE_BAD_PARAMETER =
|
|
635
|
+
/^[ \t]*bad parameter\s+(\S+)[^\n]*\(line \d+ column \d+\)/im;
|
|
636
|
+
const BARE_SYNTAX =
|
|
637
|
+
/^[ \t]*(?:syntax error|bad command name|expected [a-z]+(?: [a-z]+)*)[^\n]*\(line \d+ column \d+\)/im;
|
|
638
|
+
|
|
607
639
|
/**
|
|
608
|
-
* Classify the
|
|
609
|
-
* `:parse`
|
|
610
|
-
*
|
|
611
|
-
*
|
|
640
|
+
* Classify the printed value of `:put [:parse "<cli>"]`. Grounded on CHR 7.23.3
|
|
641
|
+
* (see GH#230): `:parse` *never* throws — it returns a value whose text carries
|
|
642
|
+
* the verdict. For a bare menu path the accepted form is `(evl /canonical/path)`
|
|
643
|
+
* (abbreviations expanded); for script-shaped input the accepted return echoes
|
|
644
|
+
* the statement body and may contain arbitrary text — including text that looks
|
|
645
|
+
* like a diagnostic. Rejections are therefore matched only in the positions
|
|
646
|
+
* RouterOS emits them (see the grammar note above): wrapped as
|
|
647
|
+
* `(evl bad parameter <name> (line N column M) /path)` /
|
|
648
|
+
* `(<%% bad command name <name> (line N column M) a;b;c)`, or bare and
|
|
649
|
+
* line-initial as `expected … (line N column M)` / `syntax error (line N column
|
|
650
|
+
* M)`. A single `:parse` covers both the syntax and the unknown-attribute
|
|
651
|
+
* (name-level) gate on every transport — REST and native API expose the same text
|
|
652
|
+
* in the `ret` value (HTTP 200 / `as-string`), the console transports print it.
|
|
653
|
+
*
|
|
654
|
+
* Throws the matching `validation/*` error; returns on a clean parse.
|
|
612
655
|
*/
|
|
613
|
-
export function classifyParseResult(
|
|
614
|
-
|
|
656
|
+
export function classifyParseResult(
|
|
657
|
+
output: string,
|
|
658
|
+
cli: string,
|
|
659
|
+
via: string = "mac-telnet",
|
|
660
|
+
): void {
|
|
661
|
+
// The `:parse` rejection carries RouterOS's authoritative byte offset
|
|
615
662
|
// (`… (line N column M)`); surface it as the structured `error.position`.
|
|
616
663
|
const position = parseRouterOsPosition(output);
|
|
617
|
-
const unknown =
|
|
664
|
+
const unknown =
|
|
665
|
+
output.match(WRAPPED_BAD_PARAMETER) ?? output.match(BARE_BAD_PARAMETER);
|
|
618
666
|
if (unknown) {
|
|
619
667
|
throw new CentrsError({
|
|
620
668
|
code: "validation/unknown-attribute",
|
|
@@ -624,13 +672,13 @@ export function classifyParseResult(output: string, cli: string): void {
|
|
|
624
672
|
context: {
|
|
625
673
|
command: cli,
|
|
626
674
|
parameter: cleanToken(unknown[1] ?? ""),
|
|
627
|
-
validationSource:
|
|
675
|
+
validationSource: `:put [:parse ...] over ${via}`,
|
|
628
676
|
detail: output,
|
|
629
677
|
},
|
|
630
678
|
...(position ? { position } : {}),
|
|
631
679
|
});
|
|
632
680
|
}
|
|
633
|
-
if (
|
|
681
|
+
if (WRAPPED_BAD_COMMAND.test(output) || BARE_SYNTAX.test(output)) {
|
|
634
682
|
throw new CentrsError({
|
|
635
683
|
code: "validation/syntax",
|
|
636
684
|
summary: "RouterOS rejected the command syntax while parsing it.",
|
|
@@ -638,7 +686,7 @@ export function classifyParseResult(output: string, cli: string): void {
|
|
|
638
686
|
"Fix the RouterOS CLI syntax (quotes, brackets, attribute form), then retry.",
|
|
639
687
|
context: {
|
|
640
688
|
command: cli,
|
|
641
|
-
validationSource:
|
|
689
|
+
validationSource: `:put [:parse ...] over ${via}`,
|
|
642
690
|
detail: output,
|
|
643
691
|
},
|
|
644
692
|
...(position ? { position } : {}),
|
|
@@ -646,10 +694,6 @@ export function classifyParseResult(output: string, cli: string): void {
|
|
|
646
694
|
}
|
|
647
695
|
}
|
|
648
696
|
|
|
649
|
-
function routerOsStringLiteral(value: string): string {
|
|
650
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
697
|
function cleanToken(token: string): string {
|
|
654
698
|
return token.replace(/^["'`]+|["'`.,;:)]+$/g, "");
|
|
655
699
|
}
|
package/src/resolver/facts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device-class fact extraction + selector matching, shared by the fan-out
|
|
3
|
-
* resolver ({@link ../resolver/selection.ts |
|
|
3
|
+
* resolver ({@link ../resolver/selection.ts | expandSelection}) and
|
|
4
4
|
* `devices list` (`src/devices.ts`) so **both surfaces match `--where` /
|
|
5
5
|
* `--near` / `--bbox` against identical semantics**.
|
|
6
6
|
*
|
package/src/resolver/index.ts
CHANGED
|
@@ -88,14 +88,33 @@ export {
|
|
|
88
88
|
unresolvedMacError,
|
|
89
89
|
} from "./mac.ts";
|
|
90
90
|
export {
|
|
91
|
-
|
|
92
|
-
type
|
|
93
|
-
type
|
|
94
|
-
|
|
91
|
+
assertNoQuickchrOverrideConflict,
|
|
92
|
+
type QuickchrConnection,
|
|
93
|
+
type QuickchrResolution,
|
|
94
|
+
quickchrConnection,
|
|
95
|
+
resolveQuickchrTarget,
|
|
96
|
+
SUPPORTED_DESCRIPTOR_VERSION,
|
|
97
|
+
} from "./quickchr-provider.ts";
|
|
98
|
+
export {
|
|
99
|
+
expandSelection,
|
|
95
100
|
isDefaultRecordTarget,
|
|
101
|
+
type SelectionExpansion,
|
|
102
|
+
type SelectionMember,
|
|
103
|
+
type SelectionResolveInput,
|
|
96
104
|
type SelectionWhereClause,
|
|
97
105
|
type TargetSelection,
|
|
98
106
|
} from "./selection.ts";
|
|
107
|
+
export {
|
|
108
|
+
type AnyServiceEndpoint,
|
|
109
|
+
isEndpointAvailable,
|
|
110
|
+
type ServiceEndpoint,
|
|
111
|
+
type ServiceEndpointAuth,
|
|
112
|
+
type ServiceEndpointMap,
|
|
113
|
+
type ServiceEndpointSource,
|
|
114
|
+
type SshEndpointAuth,
|
|
115
|
+
type SshServiceEndpoint,
|
|
116
|
+
type UnavailableEndpoint,
|
|
117
|
+
} from "./service-endpoint.ts";
|
|
99
118
|
export {
|
|
100
119
|
type CommentKvLayer,
|
|
101
120
|
parseBoolean,
|