diff --git a/GLOSSARY.txt b/GLOSSARY.txt index 099a8cc..3aee809 100644 --- a/GLOSSARY.txt +++ b/GLOSSARY.txt @@ -684,4 +684,12 @@ unioned # of the two sources: merged with per-entry provenance rather than flatt unioning # the act of the above greppable # of a generated table: one entry per line, so `grep` finds a path apos # the `'` XML entity, decoded by the CLI-Reference parser + +# #285 CLI-Reference reshape vocabulary: the second published inventory, and the +# gate values on the paths that publish twice. +llms # `llms.txt` — MikroTik's published page inventory; the only one listing the `/` leaf a branching menu's own entry lives on, so discovery is the union of it and the sitemap +musicswitch # syscap gate: the Marvell-era switch-chip menu shape — one half of the `/interface/ethernet/switch` pair published under two gates +rbswitch # syscap gate: the RouterBOARD switch-chip menu shape — the other half of that pair +prestera # Marvell Prestera switch chip — named in the `crs_prestera` syscap gate on `/interface/ethernet/switch/qos` +xfrm # `/interface/xfrm` — the IPsec transform interface, and the one published path with no gate anywhere in its ancestry to explain its absence from a CHR tree btrfs # the Linux B-tree filesystem, a `/disk/btrfs/...` menu on RouterOS 7.20+ diff --git a/commands/explain/README.md b/commands/explain/README.md index d7b63a9..2b3806b 100644 --- a/commands/explain/README.md +++ b/commands/explain/README.md @@ -173,20 +173,31 @@ today's path/verb/args split and script-vs-structured gate: The *command* half is what #228 addressed, by adding a second first-order source rather than a schema. MikroTik's CLI Reference is generated from the definition structs, and measured against the same four pinned trees it has - **zero kind contradictions** across 906 exactly-matching paths, three RouterOS - versions and two architectures; 105 of the 112 published paths absent from - every tree carry a `package`/`conditions`/`syscap` gate that predicts the - absence. `src/explain/catalog.ts` unions the two with per-entry provenance, - which is what keeps `menus.ts`'s device-confirmed floor intact rather than - diluting it. The publication is not device truth: it uses the - definition-module spelling, so `caps-man/acl/access-list` is published and - unreachable on any device, and the hand-audited alias allowlist that recovers - the CLI spelling is guarded by two generation-time assertions. So + **zero navigation-vs-command contradictions** across 968 exactly-matching + paths, three RouterOS versions and two architectures; all but one of the 102 + published paths absent from every tree carry a `package`/`conditions`/`syscap` + gate — their own or an ancestor's — that predicts the absence. + `src/explain/catalog.ts` unions the two with per-entry provenance, which is + what keeps `menus.ts`'s device-confirmed floor intact rather than diluting it. + The publication is still not device truth, but as of #285 it is at least + spelled like the CLI: MikroTik reshaped it from module pages into per-command + leaf pages whose slug *is* the CLI path, so the definition-module spellings + (`caps-man/acl/access-list`) are gone and the alias allowlist that recovered + them is empty — kept, and kept asserted, to refuse the next one loudly. So `/system/reboot` now resolves as a `command` at `/system`, and the floor is again unmoved — the residual is a path in *neither* table, such as `/disk/format-drive`, which 7.23.2 spells `/disk format` and MikroTik does not publish at all. + Discovery is the union of MikroTik's two published inventories, not the + sitemap alone. A *branching* menu is served as a trailing-slash category URL + with no `.md` of its own, while that menu's own entry is published at + `/.md` and listed only in `llms.txt` — so sitemap-only + discovery dropped 256 of 1,070 pages, silently and field-heavily. Generation + asserts that every category dir still contributes its leaf, so the next + inventory reshape fails loudly instead of shrinking the table (#285, + tikoci/rosetta#137). + There are real unknowns in how far offline parsing can go (expression grammar, scope fidelity vs `:parse`, `[]`-nesting corner cases). Grounding those unknowns was **phase 0** (#185); its findings are folded in throughout @@ -899,13 +910,14 @@ The 948-script corpus contains no such continuation-comment argument case. The s the corpus re-measurement below shows no readable/abstention movement — no `do=`/`else=`/`command=`/`script=` brace sits inside an array in the 948 scripts, so the fix closes a device-grounded false `pass` with zero corpus -blast radius. The readable set stays exactly 7,385 statements. Recognizing a bracket-leading hash +blast radius. The readable set stayed exactly 7,385 statements. Recognizing a bracket-leading hash as a comment conservatively folds two previously separate abstentions inside one foreign kernel-panic transcript (`[#1]` / `[#2]`) into its already-unknown outer statement, leaving 14,329 argument-bearing candidates and 6,944 abstentions (48.4612%). That two-statement movement is a blast-radius measurement, not the grounding evidence; the two CHR versions above are the -oracle. +oracle. (Those are the #249-era figures; the #285 catalog correction has since +moved the readable set to 7,392 — see the re-measurement below.) The same corpus run finds five new `invalid-hash` diagnostics, all in pasted non-RouterOS material: one NGINX server block, one JavaScript highlighter, one @@ -928,6 +940,37 @@ Corpus re-measurement for #249 — `bun -e 'explainCommand'` over the 948-script expected outcome; the grounding remains the 15-input CHR probe below (CHR 7.23.3 `highlight` at the `#` byte, 0/15 mismatches), not a corpus delta. +The #285 catalog correction moved those figures, and only upward. Same harness, +old table vs new: + +| | #249 | #285 | +| --- | ---: | ---: | +| `stmts` | 18,648 | 18,648 | +| `argCandidates` | 14,329 | **14,336** | +| `argReadable` | 7,385 | **7,392** | +| `abstentions` | 6,944 | 6,944 (**48.4375%**) | +| `ambiguous-statement` | 91 | **85** | +| `unresolved-statement` | 3,911 | **3,910** | +| `context-lost` | 69 | **83** | + +Six scripts move, and every statement in them is a correction. Seven statements +that read as `ambiguous`/`unknown` now resolve and their arguments read — +`/interface/monitor-traffic`, `/system/package/apply-changes`, +`/system/package/update/{check-for-updates,download,install}` — which is the +whole of the `argCandidates`/`argReadable` gain, with `abstentions` flat. Five +more read the same but with the right verb: `/ip hotspot user reset-counters +[find …]` took `hotspot` as the verb and now takes `reset-counters`, which also +reclassifies it as a write. No document changes `containsWrite` verdict. + +`context-lost` rising is the resolver **withdrawing** a certainty claim, not +losing ground. It is `info` severity and marks a statement that resolved +correctly while the document context was already unknown. All 14 are in one +script — a bare list of command paths, one per line, with no navigation +anywhere. Previously `/interface/monitor-traffic` was unreadable and so *might* +have been an absolute navigation, which R4 has REPLACE the context; certainty +was restored on that maybe. Knowing it is a command, the resolver knows it never +navigated, so the context stays as unknown as it already was. + - **Severity is fixed here, because it drives `--fail-on`.** Three buckets, and the split is not "structural vs not": - `error` — `unclosed`, `unbalanced-close`, `unterminated-string`, diff --git a/scripts/explain-catalog-data.ts b/scripts/explain-catalog-data.ts index 879d834..b4a5905 100644 --- a/scripts/explain-catalog-data.ts +++ b/scripts/explain-catalog-data.ts @@ -23,21 +23,33 @@ * other first-order source. It is generated from the definition structs — the * `typ=` strings are a dumped internal type grammar with `:0` ordinals, and * `Package` / `Conditions` / `Syscap` are build-time gates no documentation - * author writes by hand. But it publishes the DEFINITION-MODULE spelling, not - * the CLI spelling, which is why `caps-man/acl/access-list` exists there and is + * author writes by hand. + * + * It USED to publish the DEFINITION-MODULE spelling rather than the CLI + * spelling, which is why `caps-man/acl/access-list` was documented and yet * unreachable on a device over `/console/inspect`, REST and the native API - * alike (#228, five independent oracles). + * alike (#228, five independent oracles). MikroTik has since reshaped the + * publication from module pages into per-command leaf pages whose slug IS the + * CLI path, and those spellings are gone (#285). {@link ALIASES} is empty as a + * result — still asserted, still able to refuse the next one. * * So neither source is a superset and neither is "more accurate" outright. * inspect is first-order about the **CLI surface**; CLI Reference is first-order * about the **definition structs and their gates**. This table unions them and * records, per entry, which one carried it. * - * Measured across 906 exactly-matching paths, three RouterOS versions and two + * Measured across 968 exactly-matching paths, three RouterOS versions and two * architectures, the two sources have **zero kind contradictions** — they have * never disagreed about whether something is a menu or a command. That is the * assertion this generator aborts on, and the one worth watching. * + * The publication does disagree with ITSELF on a narrower axis: seven paths are + * published twice on one page under different hardware gates, and two of those + * pairs split `Directory` against `Settings Directory` (`/system/health`, + * `/interface/ethernet/switch`). Both halves say the path is navigation, so the + * row records that and drops the refinement, exactly as a disagreed gate is + * dropped rather than picked. Only a navigation-vs-command split aborts. + * * ## What is NOT here * * Arguments, flags, types, enums and descriptions. For a Directory the @@ -79,66 +91,51 @@ import { CONTAINER_TYPES } from "./restraml-trees.ts"; export const MANUAL_BASE = "https://manual.mikrotik.com"; export const SITEMAP_URL = `${MANUAL_BASE}/sitemap.xml`; +export const LLMS_TXT_URL = `${MANUAL_BASE}/llms.txt`; export const CLI_PREFIX = "/docs/cli-reference/"; const CLI_SLUG = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/; +/** + * The section landing page: the argument-type glossary prose. It is listed in + * `llms.txt` like any other `.md`, but publishes no `**Type:**` entry, so it is + * not a CLI path and the loud-fail parser would (correctly) reject it. + */ +const SECTION_INDEX_SLUG = "index"; /** * Published doc spelling -> real CLI spelling. HAND-AUDITED; the generator * never rewrites a path on its own. * - * The naive rule ("drop an interior segment until something matches a tree") is - * unsafe, and dangerously so: it maps `/interface/ethernet/poe/monitor` onto - * `/interface/ethernet/monitor`, a different command with a disjoint field set. - * `poe`, `qos`, `acl`, `controller` and `route` are real CLI menu segments, and - * an unscoped allowlist of segment names to drop is exactly the defect filed as - * tikoci/rosetta#136. + * **Empty, and correct as empty (#285).** MikroTik reshaped the CLI Reference + * from module pages — one page carrying many command headings, in the + * DEFINITION-MODULE spelling — into per-command leaf pages whose slug IS the + * CLI path. All twenty entries this list used to carry (fifteen `caps-man` + * definition modules plus `easymesh`, `serial-interface`, `ddns`, `ifaces`, + * `queues`) are gone from the source, and each one's target is now published + * directly under its real path. Re-audited against the corrected 1,070-page + * inventory: zero of the twenty sources survive, twenty of twenty targets are + * published outright. * - * Two assertions below keep this list honest, and generation ABORTS on either: + * The machinery stays, and stays asserted, because an allowlist that expects + * zero hits is exactly what should refuse the next one loudly. Generation + * ABORTS on any of these, and every abort wants a hand decision, not an entry: * + * R0 stale source — an alias whose `from` is no longer published. That is what + * caught this reshape (`/caps-man/acl/access-list` vanished) rather than + * the table quietly keeping a dead rewrite. * R1 prefix scoping — never drop segment `S` when the prefix ENDING at `S` is - * itself a published entry. This alone rejects all nine unsafe candidates, - * including the three rosetta gets wrong. + * itself a published entry. The naive rule ("drop an interior segment until + * something matches a tree") maps `/interface/ethernet/poe/monitor` onto + * `/interface/ethernet/monitor`, a different command with a disjoint field + * set; `poe`, `qos`, `acl`, `controller` and `route` are real CLI menu + * segments. An unscoped allowlist of segment names is the defect filed as + * tikoci/rosetta#136 and fixed in tikoci/rosetta#138. * R2 field overlap — where both sides publish argument names, they must - * overlap. This is the offline proof that the two spellings are the same + * overlap. This is the offline proof that two spellings are the same * command rather than a coincidental name collision. * - * Every entry here was produced by running both rules over the naive candidate - * set and keeping the survivors, then reading them. Fifteen are the `caps-man` - * definition modules (`acl`, `cfg`, `chancfg`, `dpathcfg`, `ifaceactual`, - * `ratescfg`, `remoteap`, `seccfg`, `sta`, `rule`, `controller`); the rest are - * the same shape elsewhere. + * A new hit is a signal to read the source, not to normalize it away. */ -export const ALIASES: ReadonlyMap = new Map([ - ["/caps-man/acl/access-list", "/caps-man/access-list"], - ["/caps-man/cfg/configuration", "/caps-man/configuration"], - ["/caps-man/chancfg/channel", "/caps-man/channel"], - ["/caps-man/controller/manager", "/caps-man/manager"], - ["/caps-man/controller/manager/interface", "/caps-man/manager/interface"], - ["/caps-man/dpathcfg/datapath", "/caps-man/datapath"], - [ - "/caps-man/ifaceactual/actual-interface-configuration", - "/caps-man/actual-interface-configuration", - ], - ["/caps-man/ratescfg/rates", "/caps-man/rates"], - ["/caps-man/remoteap/remote-cap", "/caps-man/remote-cap"], - ["/caps-man/remoteap/remote-cap/provision", "/caps-man/remote-cap/provision"], - [ - "/caps-man/remoteap/remote-cap/set-identity", - "/caps-man/remote-cap/set-identity", - ], - ["/caps-man/remoteap/remote-cap/upgrade", "/caps-man/remote-cap/upgrade"], - ["/caps-man/rule/provisioning", "/caps-man/provisioning"], - ["/caps-man/seccfg/security", "/caps-man/security"], - ["/caps-man/sta/registration-table", "/caps-man/registration-table"], - [ - "/interface/wifi/easymesh/wps-push-button", - "/interface/wifi/wps-push-button", - ], - ["/system/serial-interface/serial-terminal", "/system/serial-terminal"], - ["/tool/ddns/dns-update", "/tool/dns-update"], - ["/tool/graphing/ifaces/interface", "/tool/graphing/interface"], - ["/tool/graphing/queues/queue", "/tool/graphing/queue"], -]); +export const ALIASES: ReadonlyMap = new Map(); // --------------------------------------------------------------------------- // CLI-Reference ETL (vendored; see the header) @@ -175,24 +172,146 @@ function decodeXml(text: string): string { .replace(/&/g, "&"); } -/** Every CLI-Reference slug the sitemap lists, sorted and de-duplicated. */ -export function cliRefSlugs(sitemapXml: string): string[] { +/** + * The CLI-Reference path a link names, or `null` when it names something else. + * + * `base` is the document the link was read FROM, so a relative link resolves + * the way a browser would. Both inventories publish absolute URLs today, but a + * relative one must not vanish: `new URL(link)` alone throws on `/docs/…` and + * on `./app/app.md`, and this returning `null` is indistinguishable from "not a + * CLI page". Resolving against the base is what keeps the two cases apart. + */ +function cliRefPath(location: string, base: string): string | null { + let path: string; + try { + path = new URL(location, base).pathname; + } catch { + return null; + } + return path.startsWith(CLI_PREFIX) ? path : null; +} + +/** The slug a CLI-Reference page path names, or `null` when it is not a page. */ +function pageSlug(path: string): string | null { + if (path.endsWith("/")) return null; + const slug = path.slice(CLI_PREFIX.length).replace(/\.md$/, ""); + if (slug === "" || slug === SECTION_INDEX_SLUG || !CLI_SLUG.test(slug)) + return null; + return slug; +} + +/** Every directly-addressable CLI-Reference page the sitemap lists. */ +export function sitemapSlugs(sitemapXml: string): string[] { const slugs = new Set(); for (const match of sitemapXml.matchAll(/\s*([\s\S]*?)\s*<\/loc>/g)) { - const location = decodeXml(match[1] ?? ""); - let path: string; - try { - path = new URL(location).pathname; - } catch { - continue; - } - if (!path.startsWith(CLI_PREFIX) || path.endsWith("/")) continue; - const slug = path.slice(CLI_PREFIX.length); - if (slug !== "" && CLI_SLUG.test(slug)) slugs.add(slug); + const path = cliRefPath(decodeXml(match[1] ?? ""), SITEMAP_URL); + const slug = path === null ? null : pageSlug(path); + if (slug !== null) slugs.add(slug); + } + return [...slugs].sort(); +} + +/** + * Every BRANCHING menu the sitemap serves as a trailing-slash category URL, as + * the slug of the page that carries its own entry: `app/` -> `app/app`. + * + * The section root itself (`/docs/cli-reference/`) is not a menu and is skipped. + */ +export function categoryLeafSlugs(sitemapXml: string): string[] { + const slugs = new Set(); + for (const match of sitemapXml.matchAll(/\s*([\s\S]*?)\s*<\/loc>/g)) { + const path = cliRefPath(decodeXml(match[1] ?? ""), SITEMAP_URL); + if (path === null || !path.endsWith("/")) continue; + const dir = path.slice(CLI_PREFIX.length).replace(/\/$/, ""); + if (dir === "" || !CLI_SLUG.test(dir)) continue; + slugs.add(`${dir}/${dir.split("/").pop()}`); } return [...slugs].sort(); } +/** Every CLI-Reference page `llms.txt` lists. */ +export function llmsSlugs(llmsTxt: string): string[] { + const slugs = new Set(); + for (const match of llmsTxt.matchAll(/^-\s*\[[^\]]*\]\(([^)\s]+)\)/gm)) { + const path = cliRefPath(match[1] ?? "", LLMS_TXT_URL); + const slug = path === null ? null : pageSlug(path); + if (slug !== null) slugs.add(slug); + } + return [...slugs].sort(); +} + +/** + * The page inventory: the union of the two published inventories, or abort. + * + * The sitemap alone is NOT the inventory, and its shortfall is silent. A menu + * that BRANCHES is served as a trailing-slash category URL with no `.md` of its + * own — but the menu's own `Directory` entry is published, at + * `/.md` (`app/` -> `app/app.md`), and listed only in + * `llms.txt`. Sitemap-only discovery therefore dropped 256 of 1,070 pages, and + * the field-heavy quarter of them: `app/app` carries 35 argument rows, + * `caps-man/interface/interface` 79 (tikoci/rosetta#137, centrs #285). + * + * `.md`, `/index.md` and `/.md` all 404, which is what made this + * read as a navigation stub for months. A 404 on a probe is evidence about the + * probe, not about the page. + * + * The two inventories reconcile exactly, and this asserts it rather than + * trusting either one: + * + * sitemap pages 814 ∪ derived / 256 = 1,070 = llms.txt pages + * + * The category-leaf reconciliation is the ABORT, because a category dir whose + * leaf no inventory carries is a menu entry being dropped — the #285 defect + * itself, and invisible in the output. A page only one inventory lists is + * reported instead of refused: the union already carries it, so nothing is + * dropped, and `--check` fails on the new row anyway. + * + * BOTH one-sided directions are reported, but they are not symmetric, and the + * asymmetry is the shape itself. Every sitemap page is also in `llms.txt`, so a + * sitemap-only page is bare drift. `llms.txt` has 256 pages the sitemap lacks — + * that is not drift, it IS the category leaves, so reporting them all would be + * noise that buries the signal. What is reported is the residue: an `llms.txt` + * page that is neither in the sitemap nor a category leaf, i.e. one that + * appears from nowhere. + */ +export function discoverSlugs( + sitemapXml: string, + llmsTxt: string, + report: (message: string) => void = console.warn, +): string[] { + const sitemap = new Set(sitemapSlugs(sitemapXml)); + const llms = new Set(llmsSlugs(llmsTxt)); + const leaves = new Set(categoryLeafSlugs(sitemapXml)); + const inventory = new Set([...sitemap, ...llms]); + + const orphans = [...leaves].filter((slug) => !inventory.has(slug)); + if (orphans.length > 0) + throw new Error( + `${orphans.length} sitemap category dir(s) contribute no / leaf to the\n` + + "discovered inventory (sitemap ∪ llms.txt), so those menus' own entries would be\n" + + "dropped silently — the #285 defect. Re-verify the inventory by hand:\n " + + orphans.join("\n "), + ); + + const sitemapOnly = [...sitemap].filter((slug) => !llms.has(slug)); + if (sitemapOnly.length > 0) + report( + `${sitemapOnly.length} page(s) are in the sitemap but not in llms.txt — the two ` + + `published inventories no longer agree in shape: ${sitemapOnly.join(", ")}`, + ); + + const unaccounted = [...llms].filter( + (slug) => !sitemap.has(slug) && !leaves.has(slug), + ); + if (unaccounted.length > 0) + report( + `${unaccounted.length} page(s) are in llms.txt but are neither in the sitemap nor a ` + + `category leaf, so the sitemap no longer accounts for them: ${unaccounted.join(", ")}`, + ); + + return [...inventory].sort(); +} + /** * Drop the Docusaurus preamble: the H1 title and the `import {…}` lines. The * page body starts after the last import, which is what makes every remaining @@ -430,10 +549,13 @@ export function droppedSegments(from: string, to: string): number[] { * Fold the published entries onto their CLI paths, applying the alias allowlist * and asserting it. * - * Four paths are published twice on the same page (`/interface/ethernet/switch` - * variants). Same kind is required; a gate is kept only where every occurrence - * agrees, because one ungated occurrence means the path is not uniformly gated - * and claiming a gate would over-state the publication. + * Seven paths are published twice, each on one page, under different hardware + * gates — `/interface/ethernet/switch` and its four children as `musicswitch` + * and `rbswitch`, `/system/health` as `!i386` and `health`. One entry per page + * is therefore NOT an invariant, and a key on path would reject real source. A + * gate is kept only where every occurrence states it, because one ungated + * occurrence means the path is not uniformly gated and claiming the gate would + * over-state the publication. */ function foldPublished( entries: readonly PublishedEntry[], @@ -559,13 +681,21 @@ export function build( const kinds = new Set( occurrences.map((entry) => PUBLISHED_KINDS[entry.kind]), ); - if (kinds.size > 1) { + // Occurrences that disagree about navigation-vs-executable are a real + // contradiction. Occurrences that disagree only about `menu` vs `settings` + // are not: both say the path is navigation, and they differ on whether THIS + // hardware's menu holds a single record — `/system/health` is a settings + // menu on `!i386` and a directory under the `health` syscap, on one page. + // Record what they agree on, exactly as a disagreed gate is dropped rather + // than picked. Nothing downstream distinguishes the two kinds anyway + // (`isKnownMenu` accepts either), so the weaker claim costs nothing. + if (kinds.size > 1 && kinds.has("command")) { contradictions.push( `${path}: published as ${[...kinds].sort().join(" and ")} on ${occurrences.map((e) => e.slug).join(", ")}`, ); continue; } - const kind = [...kinds][0] ?? "menu"; + const kind: PathKind = kinds.size > 1 ? "menu" : ([...kinds][0] ?? "menu"); const treeType = treeTypes.get(path); if (treeType !== undefined && !agrees(kind, treeType)) { contradictions.push( @@ -706,9 +836,9 @@ export function render(rows: readonly CatalogRow[], counts: Counts): string { * Union of two first-order sources (#228): * * 1. MikroTik's published CLI Reference (\`${MANUAL_BASE}${CLI_PREFIX}\`), - * ${counts.pages} pages, ${counts.publishedEntries.toLocaleString("en-US")} entries — first-order about the definition - * structs and their build-time gates, but published in the definition-module - * spelling rather than the CLI spelling. + * ${counts.pages.toLocaleString("en-US")} pages, ${counts.publishedEntries.toLocaleString("en-US")} entries — first-order about the definition + * structs and their build-time gates. Since #285 every page is a leaf whose + * slug is the CLI path, so no spelling has to be rewritten to be looked up. * 2. Four pinned restraml \`/console/inspect\` trees * (\`https://tikoci.github.io/restraml/\`) — first-order about the CLI surface: * @@ -722,8 +852,11 @@ ${byKind("menu")} ${byKind("command")} ${byKind("settings")} * - * Zero kind contradictions between the two sources. Generation aborts if that - * ever stops holding, rather than picking a winner. + * Zero navigation-vs-command contradictions between the two sources. Generation + * aborts if that ever stops holding, rather than picking a winner. Where a path + * is published twice as a container under different hardware gates — the seven + * \`/interface/ethernet/switch\` and \`/system/health\` variants — the row records + * what the occurrences agree on. * * **This table is not a schema.** It says what a path IS — navigation, an * executable command, or a settings menu — and never what a command accepts. @@ -736,8 +869,9 @@ ${byKind("settings")} * * **Gates conjoin down a path, so read them with {@link effectiveGates}, not * row by row.** A row states only what the publication stated at that entry. - * Row-wise, ${ungatedRows} published-only paths look ungated; ancestry-aware, - * only ${residueRows} carry no published explanation for their absence at all. + * Read row-wise, ${ungatedRows} published-only paths look ungated; read with + * ancestry, the residue carrying no published explanation for its absence at + * all is ${residueRows}. * * Tree COMMANDS are not enumerated — those are the generic CRUD leaves * \`verbs.ts\` already owns. The command rows here are the published, @@ -887,10 +1021,11 @@ export interface CatalogGate { * parent's \`syscap\` applies even though the child entry states none: the gates * up a path CONJOIN, they do not override. * - * Read row-wise instead, ${ungatedRows} published-only paths look ungated, and - * #228's finding — that a published-only path almost always explains its own - * absence — would read as false. Ancestry-aware the residue is ${residueRows}, - * and those are the only published paths carrying no explanation at all. + * Read row-wise, ${ungatedRows} published-only paths look ungated; read with + * ancestry the residue is ${residueRows}. The gap between the two is the + * conjunction, and it is what a caller has to reproduce: a child's silence about + * a gate is not the absence of one. #228's finding — that a published-only path + * almost always explains its own absence — is about the ancestry-aware number. * * Still not a claim about any router. This says what MikroTik published about * applicability; only a live device knows what it has. diff --git a/scripts/gen-explain-catalog.ts b/scripts/gen-explain-catalog.ts index bb5f714..1c8e180 100644 --- a/scripts/gen-explain-catalog.ts +++ b/scripts/gen-explain-catalog.ts @@ -17,8 +17,8 @@ * bun run explain:catalog --cache=DIR # reuse/populate a local page cache * * `--check` needs network (CLI Reference plus the pinned trees), so it hangs off - * the QA workflow rather than the offline `lint:ci` gate. A full run is ~230 - * small page fetches plus 15 MB of trees, about 15 seconds. + * the QA workflow rather than the offline `lint:ci` gate. A full run is ~1,070 + * small page fetches plus 15 MB of trees, about a minute. */ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; @@ -26,9 +26,10 @@ import { isAbsolute, join, relative, resolve } from "node:path"; import { build, CLI_PREFIX, - cliRefSlugs, countRowMarkers, countTypeMarkers, + discoverSlugs, + LLMS_TXT_URL, MANUAL_BASE, parsePage, render, @@ -110,8 +111,14 @@ async function mapConcurrent( // Main // --------------------------------------------------------------------------- -const sitemapXml = await fetchTextWithRetry(SITEMAP_URL); -const slugs = cliRefSlugs(sitemapXml); +// Both inventories, because neither alone is the page set: the sitemap serves a +// branching menu as a category URL with no `.md`, and only `llms.txt` lists the +// leaf that carries that menu's own entry (#285). +const [sitemapXml, llmsTxt] = await Promise.all([ + fetchTextWithRetry(SITEMAP_URL), + fetchTextWithRetry(LLMS_TXT_URL), +]); +const slugs = discoverSlugs(sitemapXml, llmsTxt); console.log(`cli-reference: ${slugs.length} pages`); let fetched = 0; diff --git a/src/explain/catalog.ts b/src/explain/catalog.ts index ae9a02c..23c4e92 100644 --- a/src/explain/catalog.ts +++ b/src/explain/catalog.ts @@ -9,9 +9,9 @@ * Union of two first-order sources (#228): * * 1. MikroTik's published CLI Reference (`https://manual.mikrotik.com/docs/cli-reference/`), - * 228 pages, 1,051 entries — first-order about the definition - * structs and their build-time gates, but published in the definition-module - * spelling rather than the CLI spelling. + * 1,070 pages, 1,077 entries — first-order about the definition + * structs and their build-time gates. Since #285 every page is a leaf whose + * slug is the CLI path, so no spelling has to be rewritten to be looked up. * 2. Four pinned restraml `/console/inspect` trees * (`https://tikoci.github.io/restraml/`) — first-order about the CLI surface: * @@ -24,12 +24,15 @@ * * | Kind | Total | `both` | `inspect` | `published` | * | ---- | ----- | ------ | --------- | ----------- | - * | `menu` | 563 | 436 | 73 | 54 | - * | `command` | 439 | 383 | 0 | 56 | - * | `settings` | 117 | 106 | 0 | 11 | + * | `menu` | 558 | 454 | 54 | 50 | + * | `command` | 450 | 407 | 0 | 43 | + * | `settings` | 116 | 107 | 0 | 9 | * - * Zero kind contradictions between the two sources. Generation aborts if that - * ever stops holding, rather than picking a winner. + * Zero navigation-vs-command contradictions between the two sources. Generation + * aborts if that ever stops holding, rather than picking a winner. Where a path + * is published twice as a container under different hardware gates — the seven + * `/interface/ethernet/switch` and `/system/health` variants — the row records + * what the occurrences agree on. * * **This table is not a schema.** It says what a path IS — navigation, an * executable command, or a settings menu — and never what a command accepts. @@ -42,8 +45,9 @@ * * **Gates conjoin down a path, so read them with {@link effectiveGates}, not * row by row.** A row states only what the publication stated at that entry. - * Row-wise, 46 published-only paths look ungated; ancestry-aware, - * only 7 carry no published explanation for their absence at all. + * Read row-wise, 2 published-only paths look ungated; read with + * ancestry, the residue carrying no published explanation for its absence at + * all is 1. * * Tree COMMANDS are not enumerated — those are the generic CRUD leaves * `verbs.ts` already owns. The command rows here are the published, @@ -83,18 +87,18 @@ export interface CatalogEntry { * `path|kind|provenance|package|conditions|syscap` — with trailing empty * columns trimmed. Lower-cased, slash-led and sorted by path. * - * Text rather than ~1,119 object literals so that one path is one line in a + * Text rather than ~1,124 object literals so that one path is one line in a * review diff, and so the formatter has nothing to re-wrap. */ const ROWS = ` /app|menu|both|container||app -/app/cleanup|command|both|container -/app/network|menu|both|container -/app/remove|command|both|container -/app/restart|command|both|container -/app/settings|settings|both|container -/app/setup|command|both|container -/app/update|command|both|container +/app/cleanup|command|both|container||app +/app/network|menu|both|container||app +/app/remove|command|both|container||app +/app/restart|command|both|container||app +/app/settings|settings|both|container||app +/app/setup|command|both|container||app +/app/update|command|both|container||app /beep|command|both /blink|command|both||!i386 /caps-man|menu|inspect @@ -123,6 +127,7 @@ const ROWS = ` /caps-man/remote-cap/upgrade|command|both|wireless-rep /caps-man/security|menu|both|wireless-rep /certificate|menu|both +/certificate/acme-renew|command|both /certificate/add-acme|command|both /certificate/add-scep|command|both /certificate/builtin|menu|both @@ -147,8 +152,6 @@ const ROWS = ` /certificate/settings|settings|both /certificate/sign|command|both /certificate/sign-certificate-request|command|both -/colon/root|menu|published||CONSOLE_DEBUG -/colon/root/terminal|menu|published /console|menu|both /console/inspect|command|both /console/settings|settings|both @@ -161,6 +164,7 @@ const ROWS = ` /container/mounts|menu|both|container /container/repull|command|both|container /container/restart|command|both|container +/container/save|command|both|container /container/start|command|both|container /container/stop|command|both|container /container/update|command|both|container @@ -168,15 +172,15 @@ const ROWS = ` /disk/blink|command|both||!smips /disk/btrfs|menu|both||!smips|storage /disk/btrfs/filesystem|menu|both||!smips|storage -/disk/btrfs/filesystem/add-device|command|both||!smips -/disk/btrfs/filesystem/balance-cancel|command|both||!smips -/disk/btrfs/filesystem/balance-start|command|both||!smips -/disk/btrfs/filesystem/remove-device|command|both||!smips -/disk/btrfs/filesystem/replace-cancel|command|both||!smips -/disk/btrfs/filesystem/replace-device|command|both||!smips -/disk/btrfs/filesystem/reset-counters|command|both||!smips -/disk/btrfs/filesystem/scrub-cancel|command|both||!smips -/disk/btrfs/filesystem/scrub-start|command|both||!smips +/disk/btrfs/filesystem/add-device|command|both||!smips|storage +/disk/btrfs/filesystem/balance-cancel|command|both||!smips|storage +/disk/btrfs/filesystem/balance-start|command|both||!smips|storage +/disk/btrfs/filesystem/remove-device|command|both||!smips|storage +/disk/btrfs/filesystem/replace-cancel|command|both||!smips|storage +/disk/btrfs/filesystem/replace-device|command|both||!smips|storage +/disk/btrfs/filesystem/reset-counters|command|both||!smips|storage +/disk/btrfs/filesystem/scrub-cancel|command|both||!smips|storage +/disk/btrfs/filesystem/scrub-start|command|both||!smips|storage /disk/btrfs/subvolume|menu|both||!smips|storage /disk/btrfs/transfer|menu|both||!smips|storage /disk/check|command|both||!smips|storage @@ -220,13 +224,18 @@ const ROWS = ` /dude/settings|settings|both|dude /dude/vacuum-db|command|both|dude /environment|menu|both -/file|menu|inspect +/file|menu|both +/file/copy|command|both +/file/head|command|both +/file/read|command|both /file/rsync-daemon|settings|both|rose-storage /file/sync|menu|both|rose-storage /file/sync/monitor|command|both|rose-storage +/file/tail|command|both /import|command|both -/interface|menu|inspect -/interface/6to4|menu|both|ipv6 +/interface|menu|both +/interface/6to4|menu|both +/interface/blink|command|both /interface/bonding|menu|both /interface/bonding/monitor|command|both /interface/bonding/monitor-slaves|command|both @@ -270,76 +279,73 @@ const ROWS = ` /interface/dot1x/server/active|menu|both||!smips /interface/dot1x/server/state|menu|both||!smips /interface/eoip|menu|both -/interface/eoipv6|menu|both|ipv6 +/interface/eoipv6|menu|both /interface/ethernet|menu|both||i386 /interface/ethernet/blink|command|both /interface/ethernet/cable-test|command|both /interface/ethernet/monitor|command|both||i386 /interface/ethernet/poe|menu|published|||(poe or poe-in) -/interface/ethernet/poe/monitor|command|published -/interface/ethernet/poe/power-cycle|command|published|||poe -/interface/ethernet/poe/settings|settings|published|||poesettings +/interface/ethernet/poe/monitor|command|published|||(poe or poe-in) +/interface/ethernet/poe/power-cycle|command|published|||(poe or poe-in) +/interface/ethernet/poe/settings|settings|published|||(poe or poe-in) and poesettings /interface/ethernet/reset-counters|command|both /interface/ethernet/reset-mac-address|command|both -/interface/ethernet/switch|menu|both||!smips|multiswitch -/interface/ethernet/switch/acl|menu|published -/interface/ethernet/switch/acl/policer|menu|published -/interface/ethernet/switch/dscp-qos-map|menu|published -/interface/ethernet/switch/dscp-to-dscp|menu|published -/interface/ethernet/switch/egress-vlan-tag|menu|published -/interface/ethernet/switch/egress-vlan-translation|menu|published -/interface/ethernet/switch/host|menu|both|||oldswitch -/interface/ethernet/switch/ingress-port-policer|menu|published -/interface/ethernet/switch/ingress-vlan-translation|menu|published -/interface/ethernet/switch/l3hw-settings|settings|published|||crs_prestera -/interface/ethernet/switch/l3hw-settings/advanced|settings|published|||crs_prestera -/interface/ethernet/switch/l3hw-settings/advanced/monitor|command|published -/interface/ethernet/switch/l3hw-settings/monitor|command|published -/interface/ethernet/switch/mac-based-vlan|menu|published -/interface/ethernet/switch/multicast-fdb|menu|published -/interface/ethernet/switch/one2one-vlan-switching|menu|published -/interface/ethernet/switch/policer-qos-map|menu|published +/interface/ethernet/switch|menu|both +/interface/ethernet/switch/acl|menu|published|||musicswitch +/interface/ethernet/switch/acl/policer|menu|published|||musicswitch +/interface/ethernet/switch/dscp-qos-map|menu|published|||musicswitch +/interface/ethernet/switch/dscp-to-dscp|menu|published|||musicswitch +/interface/ethernet/switch/egress-vlan-tag|menu|published|||musicswitch +/interface/ethernet/switch/egress-vlan-translation|menu|published|||musicswitch +/interface/ethernet/switch/host|menu|both|||rbswitch and oldswitch +/interface/ethernet/switch/ingress-port-policer|menu|published|||musicswitch +/interface/ethernet/switch/ingress-vlan-translation|menu|published|||musicswitch +/interface/ethernet/switch/l3hw-settings|settings|published|||rbswitch and crs_prestera +/interface/ethernet/switch/l3hw-settings/advanced|settings|published|||rbswitch and crs_prestera +/interface/ethernet/switch/l3hw-settings/advanced/monitor|command|published|||rbswitch and crs_prestera +/interface/ethernet/switch/l3hw-settings/monitor|command|published|||rbswitch and crs_prestera +/interface/ethernet/switch/mac-based-vlan|menu|published|||musicswitch +/interface/ethernet/switch/multicast-fdb|menu|published|||musicswitch +/interface/ethernet/switch/one2one-vlan-switching|menu|published|||musicswitch +/interface/ethernet/switch/policer-qos-map|menu|published|||musicswitch /interface/ethernet/switch/port|menu|both /interface/ethernet/switch/port-isolation|menu|both -/interface/ethernet/switch/port-leakage|menu|published +/interface/ethernet/switch/port-leakage|menu|published|||musicswitch /interface/ethernet/switch/port/reset-counters|command|both -/interface/ethernet/switch/prbs/reset-prbs|command|published -/interface/ethernet/switch/prbs/start-prbs|command|published -/interface/ethernet/switch/prbs/stop-prbs|command|published -/interface/ethernet/switch/protocol-based-vlan|menu|published -/interface/ethernet/switch/qos|menu|published|||crs_prestera -/interface/ethernet/switch/qos-group|menu|published -/interface/ethernet/switch/qos/map|menu|published|||crs_prestera -/interface/ethernet/switch/qos/map/ip|menu|published|||crs_prestera -/interface/ethernet/switch/qos/map/vlan|menu|published|||crs_prestera -/interface/ethernet/switch/qos/monitor|command|published -/interface/ethernet/switch/qos/port|menu|published -/interface/ethernet/switch/qos/port/reset-counters|command|published -/interface/ethernet/switch/qos/priority-flow-control|menu|published|||!prestera-ac3 -/interface/ethernet/switch/qos/profile|menu|published|||crs_prestera -/interface/ethernet/switch/qos/settings|settings|published -/interface/ethernet/switch/qos/tx-manager|menu|published|||crs_prestera -/interface/ethernet/switch/qos/tx-manager/queue|menu|published|||crs_prestera -/interface/ethernet/switch/reserved-fdb|menu|published -/interface/ethernet/switch/reset-counters|command|both||!smips -/interface/ethernet/switch/rule|menu|both -/interface/ethernet/switch/shaper|menu|published -/interface/ethernet/switch/stats|settings|published||!smips -/interface/ethernet/switch/trunk|menu|published -/interface/ethernet/switch/unicast-fdb|menu|published -/interface/ethernet/switch/unicast-fdb/flush|command|published +/interface/ethernet/switch/protocol-based-vlan|menu|published|||musicswitch +/interface/ethernet/switch/qos|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos-group|menu|published|||musicswitch +/interface/ethernet/switch/qos/map|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/map/ip|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/map/vlan|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/monitor|command|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/port|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/port/reset-counters|command|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/priority-flow-control|menu|published|||rbswitch and crs_prestera and !prestera-ac3 +/interface/ethernet/switch/qos/profile|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/settings|settings|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/tx-manager|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/qos/tx-manager/queue|menu|published|||rbswitch and crs_prestera +/interface/ethernet/switch/reserved-fdb|menu|published|||musicswitch +/interface/ethernet/switch/reset-counters|command|both +/interface/ethernet/switch/rule|menu|both|||rbswitch +/interface/ethernet/switch/shaper|menu|published|||musicswitch +/interface/ethernet/switch/stats|settings|published||!smips|musicswitch +/interface/ethernet/switch/trunk|menu|published|||musicswitch +/interface/ethernet/switch/unicast-fdb|menu|published|||musicswitch +/interface/ethernet/switch/unicast-fdb/flush|command|published|||musicswitch /interface/ethernet/switch/vlan|menu|both /interface/gre|menu|both -/interface/gre6|menu|both|ipv6 +/interface/gre6|menu|both /interface/ipip|menu|both -/interface/ipipv6|menu|both|ipv6 -/interface/l2tp-client|menu|both|ppp -/interface/l2tp-client/monitor|command|both|ppp -/interface/l2tp-ether|menu|both|ppp -/interface/l2tp-ether/monitor|command|both|ppp -/interface/l2tp-server|menu|both|ppp -/interface/l2tp-server/monitor|command|both|ppp -/interface/l2tp-server/server|settings|both|ppp +/interface/ipipv6|menu|both +/interface/l2tp-client|menu|both +/interface/l2tp-client/monitor|command|both +/interface/l2tp-ether|menu|both +/interface/l2tp-ether/monitor|command|both +/interface/l2tp-server|menu|both +/interface/l2tp-server/monitor|command|both +/interface/l2tp-server/server|settings|both /interface/list|menu|both /interface/list/member|menu|both /interface/lte|menu|both||!smips @@ -357,6 +363,7 @@ const ROWS = ` /interface/lte/esim/set-nickname|command|both||!smips /interface/lte/firmware-upgrade|command|both||!smips /interface/lte/monitor|command|both||!smips +/interface/lte/run-modem-update|command|published||!smips /interface/lte/scan|command|both||!smips /interface/lte/settings|settings|both||!smips, !i386, !mips, !powerpc /interface/lte/show-capabilities|command|both||!smips @@ -368,46 +375,48 @@ const ROWS = ` /interface/mesh/fdb|menu|both /interface/mesh/port|menu|both /interface/mesh/traceroute|command|both -/interface/ovpn-client|menu|both|ppp -/interface/ovpn-client/import-ovpn-configuration|command|both|ppp -/interface/ovpn-client/monitor|command|both|ppp -/interface/ovpn-server|menu|both|ppp -/interface/ovpn-server/monitor|command|both|ppp -/interface/ovpn-server/server|menu|both|ppp -/interface/ovpn-server/server/export-client-configuration|command|both|ppp -/interface/ppp-client|menu|both|ppp -/interface/ppp-client/at-chat|command|both|ppp -/interface/ppp-client/firmware-upgrade|command|both|ppp -/interface/ppp-client/info|command|both|ppp -/interface/ppp-client/monitor|command|both|ppp -/interface/ppp-client/scan|command|both|ppp -/interface/ppp-server|menu|both|ppp -/interface/ppp-server/monitor|command|both|ppp -/interface/pppoe-client|menu|both|ppp -/interface/pppoe-client/monitor|command|both|ppp -/interface/pppoe-client/scan|command|both|ppp -/interface/pppoe-server|menu|both|ppp -/interface/pppoe-server/monitor|command|both|ppp -/interface/pppoe-server/server|menu|both|ppp -/interface/pptp-client|menu|both|ppp -/interface/pptp-client/monitor|command|both|ppp -/interface/pptp-server|menu|both|ppp -/interface/pptp-server/monitor|command|both|ppp -/interface/pptp-server/server|settings|both|ppp -/interface/pwr-link/pwr-line|menu|published|||pwrlink -/interface/pwr-link/pwr-line/blink|command|published -/interface/pwr-link/pwr-line/configure|command|published -/interface/pwr-link/pwr-line/join|command|published -/interface/pwr-link/pwr-line/leave|command|published -/interface/pwr-link/pwr-line/monitor|command|published -/interface/pwr-link/pwr-line/reset-counters|command|published -/interface/pwr-link/pwr-line/reset-mac-address|command|published -/interface/pwr-link/pwr-line/upgrade-firmware|command|published -/interface/sstp-client|menu|both|ppp -/interface/sstp-client/monitor|command|both|ppp -/interface/sstp-server|menu|both|ppp -/interface/sstp-server/monitor|command|both|ppp -/interface/sstp-server/server|settings|both|ppp +/interface/monitor-traffic|command|both +/interface/ovpn-client|menu|both +/interface/ovpn-client/import-ovpn-configuration|command|both +/interface/ovpn-client/monitor|command|both +/interface/ovpn-server|menu|both +/interface/ovpn-server/monitor|command|both +/interface/ovpn-server/server|menu|both +/interface/ovpn-server/server/export-client-configuration|command|both +/interface/ppp-client|menu|both +/interface/ppp-client/at-chat|command|both +/interface/ppp-client/firmware-upgrade|command|both +/interface/ppp-client/info|command|both +/interface/ppp-client/monitor|command|both +/interface/ppp-client/scan|command|both +/interface/ppp-server|menu|both +/interface/ppp-server/monitor|command|both +/interface/pppoe-client|menu|both +/interface/pppoe-client/monitor|command|both +/interface/pppoe-client/scan|command|both +/interface/pppoe-server|menu|both +/interface/pppoe-server/monitor|command|both +/interface/pppoe-server/server|menu|both +/interface/pptp-client|menu|both +/interface/pptp-client/monitor|command|both +/interface/pptp-server|menu|both +/interface/pptp-server/monitor|command|both +/interface/pptp-server/server|settings|both +/interface/pwr-line|menu|published|||pwrlink +/interface/pwr-line/blink|command|published|||pwrlink +/interface/pwr-line/configure|command|published|||pwrlink +/interface/pwr-line/join|command|published|||pwrlink +/interface/pwr-line/leave|command|published|||pwrlink +/interface/pwr-line/monitor|command|published|||pwrlink +/interface/pwr-line/reset-counters|command|published|||pwrlink +/interface/pwr-line/reset-mac-address|command|published|||pwrlink +/interface/pwr-line/upgrade-firmware|command|published|||pwrlink +/interface/reset-counters|command|both +/interface/sstp-client|menu|both +/interface/sstp-client/monitor|command|both +/interface/sstp-server|menu|both +/interface/sstp-server/monitor|command|both +/interface/sstp-server/server|settings|both /interface/veth|menu|both||!smips|container /interface/vlan|menu|both||!smips /interface/vpls|menu|both||!smips @@ -417,59 +426,50 @@ const ROWS = ` /interface/vxlan/fdb|menu|both /interface/vxlan/vteps|menu|both /interface/w60g|menu|published|wireless-rep||60ghz -/interface/w60g/align|command|published|wireless-rep -/interface/w60g/monitor|command|published|wireless-rep -/interface/w60g/reset-configuration|command|published|wireless-rep -/interface/w60g/scan|command|published|wireless-rep -/interface/w60g/station|menu|published|wireless-rep -/interface/w60g/station/monitor|command|published|wireless-rep -/interface/wifi|menu|both|wireless-qca -/interface/wifi/aaa|menu|both|wireless-qca -/interface/wifi/access-list|menu|both|wireless-qca -/interface/wifi/cap|settings|both|wireless-qca -/interface/wifi/capsman|settings|both|wireless-qca -/interface/wifi/capsman/remote-cap|menu|both|wireless-qca -/interface/wifi/capsman/remote-cap/provision|command|both|wireless-qca -/interface/wifi/capsman/remote-cap/set-identity|command|both|wireless-qca -/interface/wifi/capsman/remote-cap/upgrade|command|both|wireless-qca -/interface/wifi/channel|menu|both|wireless-qca -/interface/wifi/configuration|menu|both|wireless-qca -/interface/wifi/dashboard/settings|settings|published|wireless-qca -/interface/wifi/dashboard/show|command|published|wireless-qca -/interface/wifi/dashboard/show-clients|command|published|wireless-qca -/interface/wifi/dashboard/show-logs|command|published|wireless-qca -/interface/wifi/datapath|menu|both|wireless-qca -/interface/wifi/devel|command|both|wireless-qca -/interface/wifi/dpp-bootstrap-info/gen-qr-code|command|published|wireless-qca -/interface/wifi/easymesh/bss|menu|published|wireless-qca -/interface/wifi/easymesh/dpp-trigger|command|published|wireless-qca -/interface/wifi/easymesh/sta|menu|published|wireless-qca -/interface/wifi/easymesh/topology|menu|published|wireless-qca +/interface/w60g/align|command|published|wireless-rep||60ghz +/interface/w60g/monitor|command|published|wireless-rep||60ghz +/interface/w60g/reset-configuration|command|published|wireless-rep||60ghz +/interface/w60g/scan|command|published|wireless-rep||60ghz +/interface/w60g/station|menu|published|wireless-rep||60ghz +/interface/w60g/station/monitor|command|published|wireless-rep||60ghz +/interface/wifi|menu|both +/interface/wifi/aaa|menu|both +/interface/wifi/access-list|menu|both +/interface/wifi/cap|settings|both +/interface/wifi/capsman|settings|both +/interface/wifi/capsman/remote-cap|menu|both +/interface/wifi/capsman/remote-cap/provision|command|both +/interface/wifi/capsman/remote-cap/set-identity|command|both +/interface/wifi/capsman/remote-cap/upgrade|command|both +/interface/wifi/channel|menu|both +/interface/wifi/configuration|menu|both +/interface/wifi/datapath|menu|both +/interface/wifi/devel|command|both /interface/wifi/flat-snoop|command|both -/interface/wifi/frequency-scan|command|both|wireless-qca -/interface/wifi/interworking|menu|both|wireless-qca -/interface/wifi/liberate|command|both|wireless-qca -/interface/wifi/monitor|command|both|wireless-qca -/interface/wifi/network|menu|both|wireless-qca -/interface/wifi/network/radio|menu|both|wireless-qca -/interface/wifi/provisioning|menu|both|wireless-qca -/interface/wifi/radio|menu|both|wireless-qca -/interface/wifi/radio/provision|command|both|wireless-qca -/interface/wifi/radio/reg-info|command|both|wireless-qca -/interface/wifi/radio/settings|settings|both|wireless-qca -/interface/wifi/registration-table|menu|both|wireless-qca -/interface/wifi/reset-mac-address|command|both|wireless-qca -/interface/wifi/roam|command|both|wireless-qca -/interface/wifi/scan|command|both|wireless-qca -/interface/wifi/security|menu|both|wireless-qca -/interface/wifi/security/multi-passphrase|menu|both|wireless-qca -/interface/wifi/sniffer|command|both|wireless-qca -/interface/wifi/spectral-scan|command|both|wireless-qca -/interface/wifi/steering|menu|both|wireless-qca -/interface/wifi/steering/neighbor-group|menu|both|wireless-qca -/interface/wifi/trigger-radar|command|published|wireless-qca||dfstest -/interface/wifi/wps-client|command|both|wireless-qca -/interface/wifi/wps-push-button|command|both|wireless-qca +/interface/wifi/frequency-scan|command|both +/interface/wifi/interworking|menu|both +/interface/wifi/liberate|command|both +/interface/wifi/monitor|command|both +/interface/wifi/network|menu|both +/interface/wifi/network/radio|menu|both +/interface/wifi/provisioning|menu|both +/interface/wifi/radio|menu|both +/interface/wifi/radio/provision|command|both +/interface/wifi/radio/reg-info|command|both +/interface/wifi/radio/settings|settings|both +/interface/wifi/registration-table|menu|both +/interface/wifi/reset-mac-address|command|both +/interface/wifi/roam|command|both +/interface/wifi/scan|command|both +/interface/wifi/security|menu|both +/interface/wifi/security/multi-passphrase|menu|both +/interface/wifi/sniffer|command|both +/interface/wifi/spectral-scan|command|both +/interface/wifi/steering|menu|both +/interface/wifi/steering/neighbor-group|menu|both +/interface/wifi/trigger-radar|command|published|||dfstest +/interface/wifi/wps-client|command|both +/interface/wifi/wps-push-button|command|both /interface/wifiwave2|menu|inspect /interface/wifiwave2/aaa|menu|inspect /interface/wifiwave2/access-list|menu|inspect @@ -531,6 +531,7 @@ const ROWS = ` /interface/wireless/wds/monitor|command|both|wireless-rep /interface/wireless/wps-client|command|both|wireless-rep /interface/wireless/wps-push-button|command|both|wireless-rep +/interface/xfrm|menu|published /iot|menu|inspect /iot/bluetooth|menu|both|iot /iot/bluetooth/advertisers|menu|both|iot @@ -554,8 +555,8 @@ const ROWS = ` /iot/bluetooth/scanners/advertisements/clear|command|both|iot /iot/bluetooth/whitelist|menu|both|iot /iot/gpio|menu|published|iot||gpio -/iot/gpio/analog|menu|published|iot -/iot/gpio/digital|menu|published|iot +/iot/gpio/analog|menu|published|iot||gpio +/iot/gpio/digital|menu|published|iot||gpio /iot/lora|menu|both|iot /iot/lora/channels|menu|both|iot /iot/lora/joineui|menu|both|iot @@ -599,32 +600,32 @@ const ROWS = ` /ip/arp|menu|both /ip/cloud|settings|both /ip/cloud/advanced|settings|both -/ip/cloud/app/update|command|published /ip/cloud/back-to-home-file|menu|both|||cloud-vpn -/ip/cloud/back-to-home-file/settings|settings|both +/ip/cloud/back-to-home-file/settings|settings|both|||cloud-vpn +/ip/cloud/back-to-home-file/settings/remove-certificate|command|published|||cloud-vpn /ip/cloud/back-to-home-user|menu|both|||cloud-vpn -/ip/cloud/back-to-home-user/show-client-config|command|both +/ip/cloud/back-to-home-user/show-client-config|command|both|||cloud-vpn /ip/cloud/force-update|command|both -/ip/dhcp-client|menu|both|dhcp -/ip/dhcp-client/option|menu|both|dhcp -/ip/dhcp-client/release|command|both|dhcp -/ip/dhcp-client/renew|command|both|dhcp -/ip/dhcp-relay|menu|both|dhcp -/ip/dhcp-relay/monitor|command|both|dhcp -/ip/dhcp-relay/reset-counters|command|both|dhcp -/ip/dhcp-server|menu|both|dhcp -/ip/dhcp-server/alert|menu|both|dhcp -/ip/dhcp-server/alert/reset-alert|command|both|dhcp -/ip/dhcp-server/config|settings|both|dhcp -/ip/dhcp-server/lease|menu|both|dhcp -/ip/dhcp-server/lease/check-status|command|both|dhcp -/ip/dhcp-server/lease/make-static|command|both|dhcp -/ip/dhcp-server/lease/send-reconfigure|command|both|dhcp -/ip/dhcp-server/matcher|menu|both|dhcp -/ip/dhcp-server/network|menu|both|dhcp -/ip/dhcp-server/option|menu|both|dhcp -/ip/dhcp-server/option/sets|menu|both|dhcp -/ip/dhcp-server/setup|command|both|dhcp +/ip/dhcp-client|menu|both +/ip/dhcp-client/option|menu|both +/ip/dhcp-client/release|command|both +/ip/dhcp-client/renew|command|both +/ip/dhcp-relay|menu|both +/ip/dhcp-relay/monitor|command|both +/ip/dhcp-relay/reset-counters|command|both +/ip/dhcp-server|menu|both +/ip/dhcp-server/alert|menu|both +/ip/dhcp-server/alert/reset-alert|command|both +/ip/dhcp-server/config|settings|both +/ip/dhcp-server/lease|menu|both +/ip/dhcp-server/lease/check-status|command|both +/ip/dhcp-server/lease/make-static|command|both +/ip/dhcp-server/lease/send-reconfigure|command|both +/ip/dhcp-server/matcher|menu|both +/ip/dhcp-server/network|menu|both +/ip/dhcp-server/option|menu|both +/ip/dhcp-server/option/sets|menu|both +/ip/dhcp-server/setup|command|both /ip/dns|settings|both /ip/dns/adlist|menu|both /ip/dns/adlist/pause|command|both @@ -655,44 +656,46 @@ const ROWS = ` /ip/firewall/raw/reset-counters|command|both /ip/firewall/raw/reset-counters-all|command|both /ip/firewall/service-port|menu|both -/ip/hotspot|menu|inspect -/ip/hotspot/active|menu|inspect -/ip/hotspot/cookie|menu|inspect -/ip/hotspot/host|menu|inspect -/ip/hotspot/ip-binding|menu|inspect -/ip/hotspot/profile|menu|inspect -/ip/hotspot/service-port|menu|inspect -/ip/hotspot/setup|command|both|hotspot -/ip/hotspot/user|menu|inspect -/ip/hotspot/user/profile|menu|inspect -/ip/hotspot/walled-garden|menu|inspect -/ip/hotspot/walled-garden/ip|menu|inspect -/ip/ipsec|menu|both|security -/ip/ipsec/active-peers|menu|both|security -/ip/ipsec/active-peers/kill-connections|command|both|security -/ip/ipsec/identity|menu|both|security -/ip/ipsec/installed-sa|menu|both|security -/ip/ipsec/installed-sa/flush|command|both|security -/ip/ipsec/key|menu|both|security -/ip/ipsec/key/psk|menu|both|security -/ip/ipsec/key/psk/generate|command|both|security -/ip/ipsec/key/qkd|settings|both|security -/ip/ipsec/key/qkd/get-key|command|both|security -/ip/ipsec/key/qkd/get-key-cached|command|both|security -/ip/ipsec/key/qkd/get-key-with-ids|command|both|security -/ip/ipsec/key/qkd/get-status|command|both|security -/ip/ipsec/key/rsa|menu|both|security -/ip/ipsec/key/rsa/export-pub-key|command|both|security -/ip/ipsec/key/rsa/generate-key|command|both|security -/ip/ipsec/key/rsa/import|command|both|security -/ip/ipsec/mode-config|menu|both|security -/ip/ipsec/peer|menu|both|security -/ip/ipsec/policy|menu|both|security -/ip/ipsec/policy/group|menu|both|security -/ip/ipsec/profile|menu|both|security -/ip/ipsec/proposal|menu|both|security|IKE2_DEV -/ip/ipsec/settings|settings|both|security -/ip/ipsec/statistics|settings|both|security +/ip/hotspot|menu|both +/ip/hotspot/active|menu|both +/ip/hotspot/active/login|command|both +/ip/hotspot/cookie|menu|both +/ip/hotspot/host|menu|both +/ip/hotspot/host/make-binding|command|both +/ip/hotspot/ip-binding|menu|both +/ip/hotspot/profile|menu|both +/ip/hotspot/reset-html|command|both +/ip/hotspot/service-port|menu|both +/ip/hotspot/setup|command|both +/ip/hotspot/user|menu|both +/ip/hotspot/user/profile|menu|both +/ip/hotspot/user/reset-counters|command|both +/ip/hotspot/walled-garden|menu|both +/ip/hotspot/walled-garden/ip|menu|both +/ip/hotspot/walled-garden/reset-counters|command|both +/ip/hotspot/walled-garden/reset-counters-all|command|both +/ip/ipsec|menu|both +/ip/ipsec/active-peers|menu|both +/ip/ipsec/active-peers/kill-connections|command|both +/ip/ipsec/identity|menu|both +/ip/ipsec/installed-sa|menu|both +/ip/ipsec/installed-sa/flush|command|both +/ip/ipsec/key|menu|both +/ip/ipsec/key/psk|menu|both +/ip/ipsec/key/psk/generate|command|both +/ip/ipsec/key/qkd|menu|inspect +/ip/ipsec/key/rsa|menu|both +/ip/ipsec/key/rsa/export-pub-key|command|both +/ip/ipsec/key/rsa/generate-key|command|both +/ip/ipsec/key/rsa/import|command|both +/ip/ipsec/mode-config|menu|both +/ip/ipsec/peer|menu|both +/ip/ipsec/policy|menu|both +/ip/ipsec/policy/group|menu|both +/ip/ipsec/profile|menu|both +/ip/ipsec/proposal|menu|both +/ip/ipsec/settings|settings|both +/ip/ipsec/statistics|settings|both /ip/kid-control|menu|both /ip/kid-control/device|menu|both /ip/kid-control/device/reset-counters|command|both @@ -740,10 +743,10 @@ const ROWS = ` /ip/socks/connections|menu|both /ip/socks/users|menu|both /ip/socksify|menu|both -/ip/ssh|settings|both|security -/ip/ssh/export-host-key|command|both|security -/ip/ssh/import-host-key|command|both|security -/ip/ssh/regenerate-host-key|command|both|security +/ip/ssh|settings|both +/ip/ssh/export-host-key|command|both +/ip/ssh/import-host-key|command|both +/ip/ssh/regenerate-host-key|command|both /ip/tftp|menu|both /ip/tftp/settings|settings|both /ip/traffic-flow|settings|both @@ -754,58 +757,58 @@ const ROWS = ` /ip/upnp/interfaces|menu|both /ip/vrf|menu|both /ipv6|menu|inspect -/ipv6/address|menu|both|ipv6 -/ipv6/dhcp-client|menu|both|dhcp -/ipv6/dhcp-client/option|menu|both|dhcp -/ipv6/dhcp-client/release|command|both|dhcp -/ipv6/dhcp-client/renew|command|both|dhcp -/ipv6/dhcp-relay|menu|both|dhcp -/ipv6/dhcp-relay/monitor|command|both|dhcp -/ipv6/dhcp-relay/option|menu|both|dhcp -/ipv6/dhcp-relay/reset-counters|command|both|dhcp -/ipv6/dhcp-relay/routes|menu|both|dhcp -/ipv6/dhcp-server|menu|both|dhcp -/ipv6/dhcp-server/binding|menu|both|dhcp -/ipv6/dhcp-server/binding/make-static|command|both|dhcp -/ipv6/dhcp-server/binding/send-reconfigure|command|both|dhcp -/ipv6/dhcp-server/option|menu|both|dhcp -/ipv6/dhcp-server/option/sets|menu|both|dhcp +/ipv6/address|menu|both +/ipv6/dhcp-client|menu|both +/ipv6/dhcp-client/option|menu|both +/ipv6/dhcp-client/release|command|both +/ipv6/dhcp-client/renew|command|both +/ipv6/dhcp-relay|menu|both +/ipv6/dhcp-relay/monitor|command|both +/ipv6/dhcp-relay/option|menu|both +/ipv6/dhcp-relay/reset-counters|command|both +/ipv6/dhcp-relay/routes|menu|both +/ipv6/dhcp-server|menu|both +/ipv6/dhcp-server/binding|menu|both +/ipv6/dhcp-server/binding/make-static|command|both +/ipv6/dhcp-server/binding/send-reconfigure|command|both +/ipv6/dhcp-server/option|menu|both +/ipv6/dhcp-server/option/sets|menu|both /ipv6/firewall|menu|inspect -/ipv6/firewall/address-list|menu|both|ipv6 -/ipv6/firewall/connection|menu|both|ipv6 -/ipv6/firewall/filter|menu|both|ipv6 +/ipv6/firewall/address-list|menu|both +/ipv6/firewall/connection|menu|both +/ipv6/firewall/filter|menu|both /ipv6/firewall/filter/reset-counters|command|both /ipv6/firewall/filter/reset-counters-all|command|both -/ipv6/firewall/mangle|menu|both|ipv6 +/ipv6/firewall/mangle|menu|both /ipv6/firewall/mangle/reset-counters|command|both /ipv6/firewall/mangle/reset-counters-all|command|both -/ipv6/firewall/nat|menu|both|ipv6 +/ipv6/firewall/nat|menu|both /ipv6/firewall/nat/reset-counters|command|both /ipv6/firewall/nat/reset-counters-all|command|both -/ipv6/firewall/raw|menu|both|ipv6 +/ipv6/firewall/raw|menu|both /ipv6/firewall/raw/reset-counters|command|both /ipv6/firewall/raw/reset-counters-all|command|both -/ipv6/nd|menu|inspect -/ipv6/nd/prefix|menu|both|ipv6 -/ipv6/nd/prefix/default|settings|both|ipv6 -/ipv6/nd/proxy|menu|both|ipv6 -/ipv6/nd/settings|settings|both|ipv6 -/ipv6/neighbor|menu|both|ipv6 -/ipv6/pool|menu|both|ipv6 -/ipv6/pool/used|menu|both|ipv6 -/ipv6/route|menu|both|ipv6 -/ipv6/settings|settings|both|ipv6 +/ipv6/nd|menu|both +/ipv6/nd/prefix|menu|both +/ipv6/nd/prefix/default|settings|both +/ipv6/nd/proxy|menu|both +/ipv6/nd/settings|settings|both +/ipv6/neighbor|menu|both +/ipv6/pool|menu|both +/ipv6/pool/used|menu|both +/ipv6/route|menu|both +/ipv6/settings|settings|both /lcd|settings|published||!smips|lcd -/lcd/backlight|command|published||!smips -/lcd/interface|menu|published||!smips -/lcd/interface/default-wireless|command|published||!smips -/lcd/interface/display|command|published||!smips -/lcd/interface/pages|menu|published||!smips -/lcd/pin|settings|published||!smips -/lcd/recalibrate|command|published||!smips -/lcd/screen|menu|published||!smips -/lcd/show|command|published||!smips -/lcd/take-screenshot|command|published||!smips +/lcd/backlight|command|published||!smips|lcd +/lcd/interface|menu|published||!smips|lcd +/lcd/interface/default-wireless|command|published||!smips|lcd +/lcd/interface/display|command|published||!smips|lcd +/lcd/interface/pages|menu|published||!smips|lcd +/lcd/pin|settings|published||!smips|lcd +/lcd/recalibrate|command|published||!smips|lcd +/lcd/screen|menu|published||!smips|lcd +/lcd/show|command|published||!smips|lcd +/lcd/take-screenshot|command|published||!smips|lcd /log|menu|both /lora|menu|inspect /lora/channels|menu|inspect @@ -839,20 +842,20 @@ const ROWS = ` /openflow/meter|menu|both|openflow /openflow/port|menu|both|openflow /partitions|menu|published||!i386, !smips, !mmips|partitions -/partitions/activate|command|published||!i386, !smips, !mmips -/partitions/copy-to|command|published||!i386, !smips, !mmips -/partitions/repartition|command|published||!i386, !smips, !mmips -/partitions/restore-config-from|command|published||!i386, !smips, !mmips -/partitions/save-config-to|command|published||!i386, !smips, !mmips +/partitions/activate|command|published||!i386, !smips, !mmips|partitions +/partitions/copy-to|command|published||!i386, !smips, !mmips|partitions +/partitions/repartition|command|published||!i386, !smips, !mmips|partitions +/partitions/restore-config-from|command|published||!i386, !smips, !mmips|partitions +/partitions/save-config-to|command|published||!i386, !smips, !mmips|partitions /password|command|both /port|menu|both /port/remote-access|menu|both -/ppp|menu|both|ppp -/ppp/aaa|settings|both|ppp -/ppp/active|menu|both|ppp -/ppp/l2tp-secret|menu|both|ppp -/ppp/profile|menu|both|ppp -/ppp/secret|menu|both|ppp +/ppp|menu|both +/ppp/aaa|settings|both +/ppp/active|menu|both +/ppp/l2tp-secret|menu|both +/ppp/profile|menu|both +/ppp/secret|menu|both /queue|menu|inspect /queue/interface|menu|both /queue/monitor|command|both @@ -871,6 +874,8 @@ const ROWS = ` /radius/monitor|command|both /radius/reset-counters|command|both /redo|command|both +/root|menu|published||CONSOLE_DEBUG +/root/terminal|menu|published /routing|menu|inspect /routing/bfd|menu|both||BFD_AUTHENTICATION /routing/bfd/authentication|menu|published||BFD_AUTHENTICATION @@ -946,7 +951,7 @@ const ROWS = ` /routing/rip/neighbor|menu|both /routing/rip/static-neighbor|menu|both /routing/route|menu|both -/routing/route/rule|menu|both +/routing/route/rule|menu|inspect /routing/rpki|menu|both /routing/rpki/rpki-check|command|both /routing/rpki/rpki-query|command|both @@ -979,10 +984,8 @@ const ROWS = ` /system/check-installation|command|both /system/clock|settings|both /system/clock/manual|settings|both -/system/console|menu|inspect +/system/console|menu|both /system/console/screen|settings|both||i386 -/system/dashboard/settings|settings|published -/system/dashboard/show|command|published /system/default-configuration|settings|both /system/default-configuration/caps-mode-script|settings|both /system/default-configuration/custom-script|settings|both @@ -993,12 +996,15 @@ const ROWS = ` /system/gps|settings|both|gps|mmips /system/gps/monitor|command|both|gps /system/hardware|menu|inspect -/system/health|menu|both||!i386|health -/system/health/settings|settings|both||!i386, tile|health-settings -/system/health/settings/detect-fans|command|both||!i386 +/system/health|menu|both||!i386 +/system/health/settings|settings|both||!i386, tile|health and health-settings +/system/health/settings/detect-fans|command|both||!i386|health and health-settings /system/history|menu|both /system/identity|settings|both -/system/keymat-provider|menu|inspect +/system/keymat-provider|menu|both +/system/keymat-provider/qkd-get-key|command|both +/system/keymat-provider/qkd-get-key-with-ids|command|both +/system/keymat-provider/qkd-get-status|command|both /system/leds|menu|both /system/leds/settings|settings|both /system/license|settings|both @@ -1016,7 +1022,11 @@ const ROWS = ` /system/ntp/key|menu|both /system/ntp/monitor-peers|command|both /system/ntp/server|settings|both -/system/package|menu|inspect +/system/package|menu|both +/system/package/apply-changes|command|both +/system/package/disable|command|both +/system/package/downgrade|command|both +/system/package/enable|command|both /system/package/local-update|menu|both /system/package/local-update/download|command|both /system/package/local-update/download-all|command|both @@ -1024,11 +1034,17 @@ const ROWS = ` /system/package/local-update/mirror/force-check|command|both /system/package/local-update/refresh|command|both /system/package/local-update/update-package-source|menu|both -/system/package/update|menu|inspect +/system/package/uninstall|command|both +/system/package/unschedule|command|both +/system/package/update|settings|both +/system/package/update/cancel|command|both +/system/package/update/check-for-updates|command|both +/system/package/update/download|command|both +/system/package/update/install|command|both /system/ptp|menu|published||!smips|ptp -/system/ptp/monitor|command|published||!smips -/system/ptp/port|menu|published||!smips -/system/ptp/status|menu|published||!smips +/system/ptp/monitor|command|published||!smips|ptp +/system/ptp/port|menu|published||!smips|ptp +/system/ptp/status|menu|published||!smips|ptp /system/reboot|command|both /system/regulatory|settings|both /system/reset-configuration|command|both @@ -1057,24 +1073,20 @@ const ROWS = ` /system/rtrace/start|command|both /system/rtrace/stop|command|both /system/scheduler|menu|both -/system/script|menu|inspect -/system/script/environment|menu|inspect +/system/script|menu|both +/system/script/environment|menu|both /system/script/job|menu|both -/system/serial-interface/read|command|published -/system/serial-interface/start|command|published -/system/serial-interface/stop|command|published -/system/serial-interface/write|command|published /system/serial-terminal|command|both /system/shutdown|command|both -/system/ssh|command|both|security -/system/ssh-exec|command|both|security +/system/ssh|command|both +/system/ssh-exec|command|both /system/sup-output|command|both /system/swos|settings|published||!i386, !mmips, !powerpc, !tile, !smips|swos -/system/swos/load-config|command|published||!i386, !mmips, !powerpc, !tile, !smips -/system/swos/password|command|published||!i386, !mmips, !powerpc, !tile, !smips -/system/swos/reset-config|command|published||!i386, !mmips, !powerpc, !tile, !smips -/system/swos/save-config|command|published||!i386, !mmips, !powerpc, !tile, !smips -/system/swos/upgrade|command|published||!i386, !mmips, !powerpc, !tile, !smips +/system/swos/load-config|command|published||!i386, !mmips, !powerpc, !tile, !smips|swos +/system/swos/password|command|published||!i386, !mmips, !powerpc, !tile, !smips|swos +/system/swos/reset-config|command|published||!i386, !mmips, !powerpc, !tile, !smips|swos +/system/swos/save-config|command|published||!i386, !mmips, !powerpc, !tile, !smips|swos +/system/swos/upgrade|command|published||!i386, !mmips, !powerpc, !tile, !smips|swos /system/telnet|command|both /system/upgrade|menu|inspect /system/upgrade/mirror|menu|inspect @@ -1091,24 +1103,21 @@ const ROWS = ` /task/terminate|command|both /terminal|menu|inspect /tool|menu|inspect -/tool/apptraffic/stats|menu|published||!mmips, !powerpc, !smips, !mipsel -/tool/apptraffic/stats/categories|menu|published||!mmips, !powerpc, !smips, !mipsel -/tool/apptraffic/stats/clear|command|published||!mmips, !powerpc, !smips, !mipsel /tool/bandwidth-server|settings|both /tool/bandwidth-server/session|menu|both /tool/bandwidth-test|command|both /tool/calea|menu|both|calea -/tool/dns-update|command|both|advanced-tools +/tool/dns-update|command|both /tool/e-mail|settings|both /tool/e-mail/send|command|both /tool/fetch|command|both||arm64 -/tool/flood-ping|command|both|advanced-tools|!smips -/tool/graphing|menu|inspect +/tool/flood-ping|command|both||!smips +/tool/graphing|settings|both /tool/graphing/interface|menu|both /tool/graphing/queue|menu|both /tool/graphing/resource|menu|both -/tool/ip-scan|command|both|advanced-tools|!smips -/tool/mac-scan|command|both|advanced-tools|!smips +/tool/ip-scan|command|both||!smips +/tool/mac-scan|command|both||!smips /tool/mac-server|settings|both /tool/mac-server/mac-winbox|settings|both /tool/mac-server/ping|settings|both @@ -1118,9 +1127,9 @@ const ROWS = ` /tool/netinstall/cache|menu|inspect /tool/netinstall/devices|menu|inspect /tool/netinstall/settings|menu|inspect -/tool/netwatch|menu|both|advanced-tools +/tool/netwatch|menu|both /tool/ping|command|both -/tool/ping-speed|command|both|advanced-tools|!smips +/tool/ping-speed|command|both||!smips /tool/profile|command|both /tool/romon|settings|both /tool/romon/discover|command|both @@ -1160,7 +1169,7 @@ const ROWS = ` /tool/traffic-generator/stop|command|both /tool/traffic-generator/stream|menu|both /tool/traffic-monitor|menu|both -/tool/wol|command|both|advanced-tools +/tool/wol|command|both /tr069-client|settings|both|tr069-client /tr069-client/reset-tr069-config|command|both|tr069-client /undo|command|both @@ -1196,10 +1205,10 @@ const ROWS = ` /user/expire-password|command|both /user/group|menu|both /user/settings|settings|both -/user/ssh-keys|menu|both|security -/user/ssh-keys/import|command|both|security -/user/ssh-keys/private|menu|both|security -/user/ssh-keys/private/import|command|both|security +/user/ssh-keys|menu|both +/user/ssh-keys/import|command|both +/user/ssh-keys/private|menu|both +/user/ssh-keys/private/import|command|both /zerotier|menu|both|zerotier /zerotier/controller|menu|both|zerotier /zerotier/controller/member|menu|both|zerotier @@ -1311,10 +1320,11 @@ export interface CatalogGate { * parent's `syscap` applies even though the child entry states none: the gates * up a path CONJOIN, they do not override. * - * Read row-wise instead, 46 published-only paths look ungated, and - * #228's finding — that a published-only path almost always explains its own - * absence — would read as false. Ancestry-aware the residue is 7, - * and those are the only published paths carrying no explanation at all. + * Read row-wise, 2 published-only paths look ungated; read with + * ancestry the residue is 1. The gap between the two is the + * conjunction, and it is what a caller has to reproduce: a child's silence about + * a gate is not the absence of one. #228's finding — that a published-only path + * almost always explains its own absence — is about the ancestry-aware number. * * Still not a claim about any router. This says what MikroTik published about * applicability; only a live device knows what it has. diff --git a/test/fixtures/explain/cliref/README.md b/test/fixtures/explain/cliref/README.md index 87743f0..7d73977 100644 --- a/test/fixtures/explain/cliref/README.md +++ b/test/fixtures/explain/cliref/README.md @@ -1,19 +1,27 @@ # Frozen CLI-Reference pages -Two pages of MikroTik's published CLI Reference, captured verbatim on -**2026-08-06** and frozen so the catalog parser +Pages of MikroTik's published CLI Reference, frozen so the catalog parser (`scripts/explain-catalog-data.ts`, #228) can be tested without a network. -| File | Source | Why this page | -| ---- | ------ | ------------- | -| `partitions.md` | | Every shape in one small page: a body-level `#` heading that repeats the title, `Conditions` + `Syscap` gates, a `Flag` table, `Argument` and `Read-only Argument` tables, one Directory and five Commands. It is also one of the hardware-gated paths (`syscap: partitions`) that #228 is about — absent from every CHR-derived tree, and self-documented as to why. | -| `tool__mac-server.md` | | The `/tool mac-server mac-winbox` case #207 was filed on, and the only page here with `Settings Directory` entries. Filename mirrors the generator's `--cache` naming (`/` → `__`). | +| File | Source | Captured | Why this page | +| ---- | ------ | -------- | ------------- | +| `partitions.md` | | 2026-08-06 | Every shape in one small page: a body-level `#` heading that repeats the title, `Conditions` + `Syscap` gates, a `Flag` table, `Argument` and `Read-only Argument` tables, one Directory and five Commands. It is also one of the hardware-gated paths (`syscap: partitions`) that #228 is about — absent from every CHR-derived tree, and self-documented as to why. | +| `tool__mac-server.md` | | 2026-08-06 | The `/tool mac-server mac-winbox` case #207 was filed on, and the only page here with `Settings Directory` entries. Filename mirrors the generator's `--cache` naming (`/` → `__`). | +| `system__health__health.md` | | 2026-08-12 | Both #285 shapes on one page: a `/` leaf — the kind sitemap-only discovery dropped — publishing its path TWICE, as a `Settings Directory` (`!i386`) and a `Directory` (`syscap: health`). That pair is why "one entry per page" is not an invariant and why occurrences that differ only in container kind are folded rather than refused. | + +**The first two pages no longer exist at those URLs.** MikroTik reshaped the CLI +Reference from module pages into per-command leaf pages (#285, +tikoci/rosetta#137), so `partitions.md` is now six leaves under `partitions/` +and `tool/mac-server.md` is four under `tool/mac-server/`. They are kept +deliberately, in their captured module-page shape: multi-heading pages with +intra-page nesting are what prove the parser still reads that shape, and no +current page nests. Do not "refresh" them — re-capturing would silently delete +that coverage. `system__health__health.md` is the current shape. These are documentation excerpts, retained under MikroTik's manual terms for test purposes only; they are not redistributed as a product artifact. They are **not** device truth — only a live router establishes what a given RouterOS build actually accepts. -Refresh by re-fetching the two URLs above. A parser change that alters what -these produce is a deliberate change, and the tests in -`test/unit/explain-catalog.test.ts` say what the current reading is. +A parser change that alters what these produce is a deliberate change, and the +tests in `test/unit/explain-catalog.test.ts` say what the current reading is. diff --git a/test/fixtures/explain/cliref/system__health__health.md b/test/fixtures/explain/cliref/system__health__health.md new file mode 100644 index 0000000..b941fc9 --- /dev/null +++ b/test/fixtures/explain/cliref/system__health__health.md @@ -0,0 +1,90 @@ +# Health + +> ----------- + +import {ArgTableRow} from '@site/src/components/common'; +import {ArgTable} from '@site/src/components/common'; + +----------- + +## system/health +**Conditions:** !i386 +**Type:** Settings Directory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## system/health +**Conditions:** !i386 +**Syscap:** health +**Type:** Directory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/unit/explain-catalog.test.ts b/test/unit/explain-catalog.test.ts index 2bd2c4c..ac32126 100644 --- a/test/unit/explain-catalog.test.ts +++ b/test/unit/explain-catalog.test.ts @@ -29,11 +29,15 @@ import { ALIASES, build, type CatalogRow, + categoryLeafSlugs, countRowMarkers, countTypeMarkers, + discoverSlugs, droppedSegments, + llmsSlugs, type PublishedEntry, parsePage, + sitemapSlugs, } from "../../scripts/explain-catalog-data.ts"; import { type CatalogEntry, @@ -144,9 +148,15 @@ describe("explain/catalog — the generated table", () => { }); /** - * The alias layer, read from the product side: the doc spelling must NOT be in - * the table, and the CLI spelling must be — with device provenance, since - * every alias target was proven against a tree at generation time. + * The definition-module spellings, read from the product side: the doc + * spelling must NOT be in the table and the CLI spelling must be, with device + * provenance. + * + * This used to be the alias layer's job. Since #285 the source publishes the + * CLI spelling directly and the doc spellings are gone, so the same two + * assertions now hold for a different reason — which is exactly why they stay: + * a regression in either direction is visible here whichever mechanism is + * responsible. */ test("resolves the caps-man definition-module spellings", () => { expect(PATH_CATALOG.has("/caps-man/acl/access-list")).toBe(false); @@ -175,6 +185,25 @@ describe("explain/catalog — the generated table", () => { ); }); + /** + * The shipped-table half of the #285 discovery fix. `interface/ethernet/ + * switch/qos/` is a trailing-slash category URL — the sitemap carries its + * CHILDREN but never the menu itself, whose entry is published only at + * `qos/qos.md` and listed only in `llms.txt`. It is published-only, so + * sitemap-only discovery does not merely lose its gate: the row disappears, + * and with it the explanation for why no CHR tree has it. + * + * This is also the R1 path from tikoci/rosetta#136 — `qos` being a real, + * published menu segment is what makes dropping it unsafe, and it can only be + * seen to be one once the leaf is recovered. + */ + test("carries a branching menu the sitemap alone never lists", () => { + const qos = PATH_CATALOG.get("/interface/ethernet/switch/qos"); + expect(qos?.kind).toBe("menu"); + expect(qos?.provenance).toBe("published"); + expect(qos?.syscap).toBe("rbswitch and crs_prestera"); + }); + test("entries are lower-case, slash-led and sorted", () => { const paths = [...PATH_CATALOG.keys()]; expect(paths.filter((path) => path !== path.toLowerCase())).toEqual([]); @@ -253,18 +282,18 @@ describe("explain/catalog — commandVerbIndex", () => { describe("explain/catalog — effectiveGates", () => { /** * A row states only what the publication stated AT that entry, so a gate must - * be read down the whole path. `/interface/ethernet/poe/monitor` publishes no - * gate of its own, but reaching it requires `/interface/ethernet/poe`. + * be read down the whole path. `/disk/smb-share` publishes no gate of its own, + * but reaching it requires `/disk`, which is built `!smips`. */ test("inherits an ancestor's gate", () => { - const own = lookupPath(["interface", "ethernet", "poe", "monitor"]); + const own = lookupPath(["disk", "smb-share"]); + expect(own?.conditions).toBeUndefined(); + expect(own?.package).toBeUndefined(); expect(own?.syscap).toBeUndefined(); - const gates = effectiveGates(["interface", "ethernet", "poe", "monitor"]); - expect(gates.map((gate) => gate.path)).toContain("/interface/ethernet/poe"); - expect( - gates.find((gate) => gate.path === "/interface/ethernet/poe")?.syscap, - ).toBe("(poe or poe-in)"); + const gates = effectiveGates(["disk", "smb-share"]); + expect(gates.map((gate) => gate.path)).toEqual(["/disk"]); + expect(gates[0]?.conditions).toBe("!smips"); }); test("carries a path's own gate, root-first", () => { @@ -283,27 +312,24 @@ describe("explain/catalog — effectiveGates", () => { /** * The #228 headline, re-derived from the shipped table rather than quoted: * a published-only path almost always explains its own absence from a - * CHR-derived tree, and the unexplained residue is a small fixed set. Read - * row-wise the residue looks like 46; ancestry-aware it is the seven #228 - * named. Those seven are the ONLY evidence that the publication can carry a - * path with no explanation at all, so a growing set would be a real event. + * CHR-derived tree, and the unexplained residue is a small fixed set. + * + * The corrected inventory (#285) sharpened this rather than moving it. The + * seven paths #228 named were an artifact of the module-page corpus: + * `serial-interface` and `dashboard` are now published as their own gated + * menus, and the recovered `/` leaves carry the gates their + * children inherit. Row-wise the residue is 2; ancestry-aware it is + * `/interface/xfrm` alone — the one published path that explains its absence + * from a CHR tree in no way at all. A growing set would be a real event. */ - test("the unexplained residue is the seven paths #228 named", () => { + test("the unexplained residue is one path", () => { const residue = [...PATH_CATALOG] .filter(([path, entry]) => { if (entry.provenance !== "published") return false; return effectiveGates(path.split("/").filter(Boolean)).length === 0; }) .map(([path]) => path); - expect(residue).toEqual([ - "/ip/cloud/app/update", - "/system/dashboard/settings", - "/system/dashboard/show", - "/system/serial-interface/read", - "/system/serial-interface/start", - "/system/serial-interface/stop", - "/system/serial-interface/write", - ]); + expect(residue).toEqual(["/interface/xfrm"]); }); }); @@ -351,6 +377,38 @@ describe("explain/catalog — parsing published pages", () => { ]); }); + /** + * A `/` leaf, the shape sitemap-only discovery dropped (#285), + * publishing ONE path as two entries under different hardware gates. Read + * end-to-end, because each half was decided from this page: the parser keeps + * both occurrences, the fold records the kind they agree on, and the gate + * survives only where both state it — `!i386` does, `syscap: health` does not. + */ + test("reads a path published twice under different gates", async () => { + const entries = parsePage( + "system/health/health", + await readFixture("system__health__health.md"), + ); + expect( + entries.map((entry) => `${entry.path} ${entry.kind} ${entry.syscap}`), + ).toEqual([ + "/system/health Settings Directory null", + "/system/health Directory health", + ]); + + const rows = buildWith(entries, { "/system/health": "dir" }); + expect(rows).toEqual([ + { + conditions: "!i386", + kind: "menu", + package: undefined, + path: "/system/health", + provenance: "both", + syscap: undefined, + }, + ]); + }); + /** Flag rows name print flags, not arguments; only R2 consumes this set. */ test("collects argument names but not print flags", async () => { const entries = parsePage("partitions", await readFixture("partitions.md")); @@ -465,6 +523,127 @@ describe("explain/catalog — parsing published pages", () => { }); }); +// --------------------------------------------------------------------------- +// Discovery +// --------------------------------------------------------------------------- + +/** + * The sitemap is not the inventory (#285). A branching menu is served as a + * trailing-slash category URL with no `.md`, while the menu's own entry is + * published at `/.md` and listed only in `llms.txt`. These + * hold the shape of that pair, because getting it wrong shrinks the catalog by + * a quarter without a single error. + */ +describe("explain/catalog — discovery", () => { + const loc = (path: string): string => + `https://manual.mikrotik.com${path}`; + const sitemap = [ + loc("/docs/cli-reference/"), + loc("/docs/cli-reference/app/"), + loc("/docs/cli-reference/app/cleanup"), + loc("/docs/cli-reference/beep"), + loc("/docs/other/thing"), + ].join("\n"); + const llms = [ + "- [App](https://manual.mikrotik.com/docs/cli-reference/app/app.md)", + "- [Cleanup](https://manual.mikrotik.com/docs/cli-reference/app/cleanup.md)", + "- [Beep](https://manual.mikrotik.com/docs/cli-reference/beep.md)", + "- [Index](https://manual.mikrotik.com/docs/cli-reference/index.md)", + "- [Other](https://manual.mikrotik.com/docs/other/thing.md)", + ].join("\n"); + + test("the sitemap lists only the directly-addressable pages", () => { + expect(sitemapSlugs(sitemap)).toEqual(["app/cleanup", "beep"]); + }); + + test("a category URL names the leaf that carries the menu's own entry", () => { + expect(categoryLeafSlugs(sitemap)).toEqual(["app/app"]); + }); + + /** `index` is the argument-type glossary prose, not a CLI path. */ + test("llms.txt lists every page except the section landing page", () => { + expect(llmsSlugs(llms)).toEqual(["app/app", "app/cleanup", "beep"]); + }); + + /** + * Both inventories publish absolute URLs today, so a relative link must be + * resolved rather than dropped: `new URL(link)` alone throws on it, and + * returning nothing is indistinguishable from "not a CLI page" — which is how + * an inventory silently shrinks. Root-relative and document-relative both + * resolve against `llms.txt`'s own URL, the way a browser reads them. + */ + test("a relative llms.txt link resolves against the document, not away", () => { + expect( + llmsSlugs( + [ + "- [Root-relative](/docs/cli-reference/beep.md)", + "- [Document-relative](./docs/cli-reference/app/app.md)", + "- [Elsewhere](/docs/other/thing.md)", + ].join("\n"), + ), + ).toEqual(["app/app", "beep"]); + }); + + test("the inventory is the union, and the category leaf survives it", () => { + expect(discoverSlugs(sitemap, llms)).toEqual([ + "app/app", + "app/cleanup", + "beep", + ]); + }); + + /** + * The #285 defect itself, refused. A category dir whose leaf no inventory + * carries means that menu's entry is being dropped — invisibly, since the + * generated table would simply be shorter. + */ + test("aborts when a category dir contributes no leaf", () => { + expect(() => + discoverSlugs(sitemap, llms.split("\n").slice(1).join("\n")), + ).toThrow(/contribute no \/ leaf/); + }); + + /** + * A page only the sitemap lists is reported, not refused: the union already + * carries it, so nothing is dropped, and `--check` fails on the new row. + */ + test("reports a sitemap page llms.txt does not list", () => { + const reported: string[] = []; + const slugs = discoverSlugs( + `${sitemap}\n${loc("/docs/cli-reference/new-page")}`, + llms, + (message) => reported.push(message), + ); + expect(slugs).toContain("new-page"); + expect(reported.join("\n")).toMatch(/not in llms\.txt.*new-page/s); + }); + + /** + * And the inverse, which is NOT symmetric. `llms.txt` legitimately carries + * every category leaf the sitemap lacks — 256 of them — so reporting all + * llms-only pages would bury the signal in the shape. The residue is what + * matters: a page in neither the sitemap nor the category leaves has appeared + * from nowhere and the sitemap no longer accounts for it. + */ + test("reports an llms.txt page the sitemap cannot account for", () => { + const reported: string[] = []; + const slugs = discoverSlugs( + sitemap, + `${llms}\n- [Nowhere](https://manual.mikrotik.com/docs/cli-reference/nowhere.md)`, + (message) => reported.push(message), + ); + expect(slugs).toContain("nowhere"); + expect(reported.join("\n")).toMatch(/neither in the sitemap.*nowhere/s); + }); + + /** ...and the category leaves themselves are never reported as that. */ + test("stays silent when llms.txt differs only by its category leaves", () => { + const reported: string[] = []; + discoverSlugs(sitemap, llms, (message) => reported.push(message)); + expect(reported).toEqual([]); + }); +}); + // --------------------------------------------------------------------------- // The generator's refusals // --------------------------------------------------------------------------- @@ -528,15 +707,32 @@ describe("explain/catalog — what the generator refuses", () => { ).toThrow(/disagree about what these paths ARE/); }); + /** + * ...but a `Directory` / `Settings Directory` pair is NOT that disagreement. + * Both say the path is navigation and differ only on whether this hardware's + * menu holds a single record — `/system/health` is published both ways on one + * page, `!i386` and `health`. Record what they agree on, as a disagreed gate + * is dropped rather than picked. + */ + test("records the agreed kind when two occurrences differ only in container kind", () => { + const rows = buildWith( + [entry("/x/y", "Settings Directory"), entry("/x/y", "Directory")], + {}, + ); + expect(rows.map((row) => `${row.path} ${row.kind}`)).toEqual(["/x/y menu"]); + }); + /** * R1, stated directly. `poe` is a real CLI menu segment, so dropping it is - * never allowed — this is the rule tikoci/rosetta#136 is missing, and the - * reason nine of the naive candidates are not in {@link ALIASES}. + * never allowed — this is the rule tikoci/rosetta#136 was missing. + * + * Exercised against a synthetic allowlist because the shipped one is empty + * (#285): the rule guards the next entry someone adds, so it must be provable + * without one. */ test("R1: refuses to drop a segment that is itself a published entry", () => { const from = "/caps-man/acl/access-list"; const to = "/caps-man/access-list"; - expect(ALIASES.get(from)).toBe(to); expect(() => buildWith( [ @@ -614,8 +810,14 @@ describe("explain/catalog — what the generator refuses", () => { ).toThrow(/is in no pinned tree/); }); - /** Every alias must be a pure segment drop, or R1 is not defined on it. */ - test("every allowlist entry is a segment drop", () => { + /** + * The allowlist is empty and that is the correct state (#285): MikroTik now + * publishes every path in its CLI spelling, so nothing needs rewriting. Pinned + * because an entry appearing again is a source change to read, not a routine + * regeneration — and because R0/R1/R2 above only ever fire on an entry. + */ + test("the allowlist is empty, and any entry would be a segment drop", () => { + expect([...ALIASES]).toEqual([]); for (const [from, to] of ALIASES) expect(droppedSegments(from, to).length).toBeGreaterThan(0); expect(() => droppedSegments("/a/b/c", "/a/x")).toThrow( diff --git a/test/unit/explain-write.test.ts b/test/unit/explain-write.test.ts index b00a101..86739e6 100644 --- a/test/unit/explain-write.test.ts +++ b/test/unit/explain-write.test.ts @@ -238,13 +238,30 @@ describe("explain/write — finding 2: fail closed where input is discarded", () * unconfirmed-nav fallback and instead arrives with V4 ambiguity. Clearing it * would be a false negative on a curated write verb, so it abstains — Q14's * rule (b) enforced at the rollup. + * + * This read `/interface\nreset-counters` until #285; `reset` is the same shape + * with the path still in neither table, which is what this test is about. The + * head that MOVED has its own test below. */ test("a bare-word head abstains rather than clearing the document", () => { - const got = containsWrite("/interface\nreset-counters"); + const got = containsWrite("/interface\nreset"); expect(got.verdict).toBe("unknown"); expect(got.blockers.map((b) => b.klass)).toEqual(["ambiguous"]); }); + /** + * The other side of that boundary, on its own anchor so a catalog change is + * legible as one. The corrected CLI-Reference inventory (#285) publishes + * `/interface/reset-counters` as a Command and the pinned trees carry it too, + * so the head RESOLVES and the document is a confirmed write — the abstention + * that used to stand here was a false negative the larger table fixed. + */ + test("a bare-word head the catalog knows resolves instead", () => { + const got = containsWrite("/interface\nreset-counters"); + expect(got.verdict).toBe("true"); + expect(got.blockers).toEqual([]); + }); + /** * The P1 from the Codex review round. `isConfirmedNav` decides at STATEMENT * scale, so it dropped a lone `/system/reboot` as navigation and cleared the @@ -396,9 +413,14 @@ describe("explain/write — the published command axis (#228)", () => { expect(python.occurrences.map((o) => o.verb)).toEqual([null, null]); }); - /** Both tables are floors: absence from them is still not evidence. */ + /** + * Both tables are floors: absence from them is still not evidence. + * + * `/interface/reset` is the anchor since #285 — `/interface/reset-counters`, + * which this used to use, is now published and therefore decided. + */ test("a path in neither table is unchanged", () => { - const got = containsWrite("/interface\nreset-counters"); + const got = containsWrite("/interface\nreset"); expect(got.verdict).toBe("unknown"); expect(got.blockers.map((b) => b.klass)).toEqual(["ambiguous"]); });