Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/instructions/local-db-grounding.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
description: "In a checkout, the resolved ros-help.db is untrusted until verified; the latest CI release DB is the grounding source of truth."
applyTo: "src/paths.ts, src/setup.ts, src/mcp.ts, scripts/db-doctor.ts, Makefile, MANUAL.md, VALIDATION.md"
---
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Local DB grounding

Dev mode (`resolveDbPath` → project root) serves the repo-root `ros-help.db` — a
git-ignored file whose contents are arbitrary local state. **It is not trusted by
default.** Before grounding any claim about shipped data on it, verify it.

## The rule

- **The latest CI-built release DB is the source of truth for grounding claims.**
It is what `bunx @tikoci/rosetta` consumers actually get. `make db-sync`
(`scripts/db-sync.ts`) fetches it into the resolved path (atomic replace — safe
while an MCP server holds the old file open).
- It does **not** use `--refresh`: that pins the URL to `package.json`'s
version, which in a checkout is a CI-rewritten placeholder (`v0.11.0-rc.0`)
with no release, and falls back to `/releases/latest` — the newest *stable*,
which lags the prerelease schema. `db-sync` instead uses `gh` to find the
newest release (prereleases **included**) that actually ships
`ros-help.db.gz`, then reuses the hardened `downloadDb()` (schema/content
validation + stale-sidecar cleanup + atomic swap) via its `urlsOverride`
param. Requires the `gh` CLI. Keep it this way.
- **`make extract` produces an *unstamped* local working DB** — for extraction
and pipeline work, not for claims about shipped data. `db-doctor` reports it as
`unstamped`; that is expected, not a failure to "fix" by re-extracting.
- **Verify before trusting.** `make db-doctor` (or `bun run db:doctor`) prints the
resolved path + `db_meta` provenance + a grounding verdict and exits non-zero
when not `ok`. In-session, one `routeros_stats` call returns the same
`provenance.grounding` block — prefer it over shelling into sqlite.

## The verdict (`classifyDbGrounding`, `src/paths.ts`)

Pure function, shared by `routeros_stats` (`getDbStats`), MCP startup, and
`db-doctor`. Status precedence:

| status | meaning | typical fix |
|---|---|---|
| `schema_mismatch` | `PRAGMA user_version` ≠ code `SCHEMA_VERSION` | `make db-sync` |
| `internal_inconsistent` | `db_meta.schema_version` ≠ pragma — a corpus bumped in place by `initDb()`; provenance no longer describes the bytes (the #94 "Frankenstein") | `make db-sync` |
| `unstamped` | neither `release_tag` nor `source_commit` — a local `make extract` build | fine for extraction; `make db-sync` to ground |
| `provenance_incomplete` | claims release identity but is missing one of the four CI stamps (`release_tag`/`source_commit`/`built_at`/`schema_version`), or a stamped version that won't parse — fail closed | `make db-sync` |
| `tag_behind` | `release_tag` base version behind the checkout | `make db-sync` |
| `ok` | all four stamps present, schema coherent, tag current | — |

`ok` means the DB is **schema/release-compatible** with this build (coherent schema, complete provenance, release not behind) — not proof it was built from the exact checked-out commit. A release DB is legitimately built from an ancestor commit; `db-doctor` reports that source commit for inspection.

The rc/beta counter is intentionally ignored — a dev checkout's `package.json`
routinely reads `-rc.0` while the correct published DB is a much higher rc. Only
the `MAJOR.MINOR.PATCH` base is a staleness axis.

## Startup behavior

In dev mode, MCP startup emits a **loud but non-fatal** banner when the verdict
is not `ok` (never fetches — a contributor's local build must not be clobbered).
`checkDbFreshness` still hard-fails a genuine schema mismatch and auto-redownloads
in package/compiled mode; this warning is purely additive surfacing.

Keep the classifier, its three consumers, and this file in sync when the verdict
shape or `db_meta` key set changes. Cross-reference: `db-meta-stamping.instructions.md`.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ uses [Semantic Versioning](https://semver.org/).

### Added

- **The resolved DB now reports whether it can be trusted to ground claims about the code querying it** (issue #94, B-0022 export-audit umbrella). `routeros_stats` gains a `provenance` block — resolved `db_path`, invocation `mode`, the `db_meta` stamps (`release_tag`/`source_commit`/`built_at`/`schema_version`), and a **grounding verdict** (`ok` / `schema_mismatch` / `internal_inconsistent` / `tag_behind` / `unstamped`) — so one call self-checks the DB against the checkout instead of shelling into sqlite (the TUI `stats` screen shows it too, via shared `getDbStats`). A new `make db-doctor` / `bun run db:doctor` prints the same verdict and exits non-zero when not `ok` (CI/pre-commit-ready); `make db-sync` / `bun run db:sync` fetches the latest CI release DB — the documented grounding source of truth — into the resolved path (atomic, safe while a server holds the file open). In a dev checkout, MCP startup now emits a loud but **non-fatal** banner when the served DB has drifted (e.g. a corpus whose `PRAGMA user_version` was bumped in place so its stamped provenance no longer describes its bytes); it never fetches, so a contributor's local build is never clobbered. The load-bearing new signal is `internal_inconsistent` (`db_meta.schema_version` ≠ pragma), which every prior check missed. Durable guidance lives in `.github/instructions/local-db-grounding.instructions.md`.

- **New `rosetta export <dir>` command writes a DB-only dataset directory for local audit** (issue #101, the B-0022 export track). Produces `manifest.toml` plus seven flat TSVs — `changelog.tsv`, `callouts.tsv`, `properties.tsv` (joined to pages with a resolved `section_anchor`), `videos.tsv` (metadata + per-video transcript segment/word/byte counts), `commands.tsv`, and the paired `pages.tsv` (one row per page) / `sections.tsv` (one row per section, pivot-able) — from the runtime SQLite database alone — dataset generation reads only the resolved DB, with no caches or re-extraction (startup may still download/refresh the DB, as every command does). The serialization contract is a reversible Postgres-COPY-style escape (`\t`/`\n`/`\r`/`\\` backstopped on every value, whole-field `\N` for SQL NULL as distinct from the empty string) with stable ordering so a rebuild on the same DB is byte-identical, and `manifest.toml` carries `db_meta` provenance, per-file row counts, and honest disclosures of what the DB cannot provide (transcript provenance #21, per-version architecture #91). A column the DB cannot produce is omitted and disclosed, never recovered from a source artifact.

- **`rosetta export` now emits `tables.tsv`, an inventory of every captured doc table** (issue #111, under the #104 B-0022 export track). One row per `page_tables` record (855 on the rc.99 corpus) with page/section provenance, the table's shape (`column_count`/`data_row_count`/`is_ragged`), source size (`raw_bytes`, UTF-8 length of the stored Markdown), and a `table_url` that deep-links to the section containing the table (`url#anchor`; the bare page URL when no section resolves, since Docusaurus has no per-table anchor). This is the "what tables exist and where" list — it answers most table-audit questions without exporting the cell data itself (the per-fragment cell files remain E4). Reuses the settled TSV/manifest contract and is pruned/owned the same way as every other dataset.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Agent-specific entrypoints are thin shims: Claude starts here, Codex starts with
- Need **manual-site migration context**? MikroTik moved future docs to Docusaurus at <https://manual.mikrotik.com>; read `DESIGN.md` and `briefings/B-0012-docusaurus-manual-migration.md` before touching extraction, MCP, or TUI search.
- Need **external agent-benchmark context**? `~/GitHub/bench-routeros-tools` tests rosetta as one grounding approach for live RouterOS command generation; read `DESIGN.md` → "External benchmark feedback loop" before changing retrieval, skills, or command-validation surfaces.
- Need **current corpus counts**? Use `routeros_stats` rather than hard-coded numbers here.
- Need to **trust the local DB before grounding a claim**? In a checkout, repo-root `ros-help.db` is untrusted until verified. Run `make db-doctor` (or read `routeros_stats` → `provenance.grounding`); `make db-sync` fetches the latest CI release DB (the grounding source of truth). See `.github/instructions/local-db-grounding.instructions.md`.
- Need **work status or open ideas**? Use `gh issue list` (active work), `briefings/`, and `BACKLOG.md`; `tasks/` is a frozen archive.

Nothing else should be duplicated here. If this file starts growing operational detail, schema blocks, or long rule lists again, move that content back into its canonical home instead.
10 changes: 10 additions & 0 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ The database combines multiple MikroTik data sources into a single SQLite file w

Documentation covers RouterOS **v7 only**. v6 had different syntax and major subsystems — answers for v6 are unreliable. Prose is now sourced live from <https://manual.mikrotik.com>, which also has a CLI Reference with `/console/inspect`-derived command menus and argument types (not yet ingested — see B-0012's proposed follow-up tasks).

## Local DB grounding (dev checkouts)

In a checkout, dev mode serves the repo-root `ros-help.db` — a git-ignored file whose contents are arbitrary local state (a leftover `make extract` build, a partially-migrated corpus, or a schema bumped in place over stale data). **It is not trusted by default.** Before grounding a claim about shipped data on it, verify it.

- **`make db-doctor`** (or `bun run db:doctor`) prints the resolved DB path, its `db_meta` provenance, and a grounding verdict; exits non-zero when the verdict is not `ok`. In-session, one `routeros_stats` call returns the same `provenance.grounding` block.
- **`make db-sync`** (or `bun run db:sync`) fetches the newest CI-built release DB **matching this checkout's schema** into the resolved path — the **source of truth** for grounding claims, since it is what `bunx @tikoci/rosetta` consumers get. It uses `gh` to find the newest release that ships the DB asset, prereleases included (not `--refresh`, which would grab the newest *stable* and its older schema). The replace is atomic, so it is safe to run while an MCP server holds the old file open. Requires the `gh` CLI.
- A local `make extract` build is intentionally **unstamped** (no `release_tag`/`source_commit`) — fine for extraction and pipeline work, but `db-doctor` reports it as `unstamped` to signal it is not a grounding source.

Verdict statuses: `schema_mismatch` (pragma ≠ code schema), `internal_inconsistent` (`db_meta.schema_version` ≠ pragma — a corpus bumped in place, the classic "which DB is this?" trap), `unstamped` (a local build with no release stamps), `provenance_incomplete` (claims release identity but is missing one of the four CI stamps — fail closed), `tag_behind`, and `ok`. `ok` means schema/release-**compatible** with this build, not that the DB was built from the exact checked-out commit (a release DB is built from an ancestor). In dev mode, MCP startup emits a loud but non-fatal banner when the verdict is not `ok` (it never fetches — a contributor's local build is never clobbered). See `.github/instructions/local-db-grounding.instructions.md`.

## Re-extracting a Local Database

`make extract` and `make extract-full` fetch `/docs` prose live from manual.mikrotik.com and rebuild the versioned command tree, devices, changelogs, Dude cache, and skills:
Expand Down
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ VERSION ?=
link gc-versions assess assess-hardware assess-www device-map device-map-check \
extract-hardware-catalog search browse serve \
typecheck lint test preflight verify \
db-doctor db-sync \
install setup clean eval eval-update eval-self eval-self-update

# ── Development ──
Expand All @@ -21,6 +22,22 @@ install:
serve:
bun run src/mcp.ts

# Report whether the resolved ros-help.db matches this checkout (#94). Prints the
# resolved path + db_meta provenance + a grounding verdict; non-zero when not "ok".
# The latest CI-built release DB is the source of truth for grounding claims — a
# local `make extract` build is for extraction work, not for claims about shipped data.
db-doctor:
bun run scripts/db-doctor.ts

# Fetch the newest CI-built release DB that matches this checkout's schema into
# the resolved DB path (dev: repo root), so the MCP server / db-doctor ground on
# what npm consumers actually get. Discovers the newest release shipping the DB
# asset via `gh` (prereleases included — `--refresh`/`/releases/latest` would
# grab the newest *stable*, an older schema). Atomic replace — safe to run while
# an MCP server holds the old file open. Requires the `gh` CLI.
db-sync:
bun run scripts/db-sync.ts

search:
bun run src/search.ts $(query)

Expand Down
1 change: 1 addition & 0 deletions VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Tasks in `tasks/` reference these IDs in their `validation:` frontmatter. When a
| V-db-min-content | Built DB has ≥200 pages, ≥1000 commands, ≥100 devices, ≥1000 properties, ≥800 generic page tables, ≥8000 table data rows, ≥4000 properties linked to exact table rows, ≥200 hardware_catalog, ≥600 device_aliases before publish | `qa.yml` db-content gate (single definition of the floors; run by `release.yml`'s `qa` job with `db_source=artifact`, so it floors the exact DB about to publish) | blocking | — |
| V-db-wipe-guard | Tests cannot accidentally open and overwrite the on-disk DB | `src/query.test.ts` `:memory:` guard + post-extraction guard in CI | blocking | — |
| V-db-meta | Released DB carries `db_meta` provenance (release_tag, built_at, source_commit, schema_version) | `scripts/stamp-db-meta.ts` in `release.yml`'s `build` job; presence re-checked by `qa.yml`'s db-meta gate | blocking | — |
| V-db-grounding | The grounding verdict (`classifyDbGrounding`) correctly classifies a schema-coherent-but-stale corpus as `internal_inconsistent`, an unstamped local build as `unstamped`, and a coherent CI artifact as `ok`; `make db-doctor` exits non-zero on any non-`ok` verdict | `src/paths.test.ts` (`classifyDbGrounding` cases) + `src/query.test.ts` (`getDbStats` provenance) in `bun test`; `scripts/db-doctor.ts` exit code | blocking (unit) | #94 |
| V-bunx-macos | `bunx @tikoci/rosetta` installs + serves on macOS | `bunx-smoke` matrix in `release.yml` | blocking | — |
| V-bunx-linux | Same on Linux | `bunx-smoke` matrix in `release.yml` | blocking | — |
| V-bunx-windows | Same on Windows — catches v0.8.x EBUSY / readonly-WAL / temp-file class of bugs | `windows-latest` row in `bunx-smoke` matrix in `release.yml` | blocking | — |
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"lint": "biome check src/",
"lint:md": "markdownlint-cli2 \"**/*.md\"",
"lint:spell": "cspell lint . --no-progress",
"browse": "bun run src/mcp.ts browse"
"browse": "bun run src/mcp.ts browse",
"db:doctor": "bun run scripts/db-doctor.ts",
"db:sync": "bun run scripts/db-sync.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
Expand Down
59 changes: 59 additions & 0 deletions scripts/db-doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bun

/**
* db-doctor.ts — report whether the resolved ros-help.db can be trusted to
* ground claims about this checkout (#94).
*
* Resolves the DB exactly as the MCP server would (DB_PATH / --db / auto-detect),
* reads its db_meta provenance, and prints a grounding verdict. Exits non-zero
* when the verdict is not "ok", so it doubles as a CI/pre-commit gate.
*
* Usage:
* bun run scripts/db-doctor.ts # resolved DB (dev: repo-root ros-help.db)
* DB_PATH=/path/to/ros-help.db bun run scripts/db-doctor.ts
*
* Fix a stale/mismatched DB with `make db-sync` (fetches the latest CI release DB).
*/

import { classifyDbGrounding, detectMode, resolveDbPath, resolveVersion, SCHEMA_VERSION } from "../src/paths.ts";
import { probeDb } from "../src/setup.ts";

const dbPath = resolveDbPath(import.meta.dirname);
const mode = detectMode(import.meta.dirname);
const codeVersion = resolveVersion(import.meta.dirname);

console.log(`rosetta db-doctor`);
console.log(` code : v${codeVersion} (schema v${SCHEMA_VERSION}, mode ${mode})`);
console.log(` resolved DB : ${dbPath}`);

const p = probeDb(dbPath);
if (!p) {
console.log(` status : NO_DB — no readable SQLite database at the resolved path.`);
console.log(` fix : run 'make db-sync' to fetch the latest CI release DB.`);
process.exit(1);
}

const verdict = classifyDbGrounding({
pragmaSchema: p.schemaVersion,
metaSchema: p.metaSchemaVersion,
releaseTag: p.releaseTag,
builtAt: p.builtAt,
sourceCommit: p.sourceCommit,
codeSchema: SCHEMA_VERSION,
codeVersion,
mode,
});

console.log(` release_tag : ${p.releaseTag ?? "(unstamped)"}`);
console.log(` source_commit: ${p.sourceCommit ?? "(unstamped)"}`);
console.log(` built_at : ${p.builtAt ?? "(unstamped)"}`);
console.log(` schema : pragma=${p.schemaVersion} meta=${p.metaSchemaVersion ?? "—"} code=${SCHEMA_VERSION}`);
console.log(` pages : ${p.pages} commands: ${p.commands}`);
console.log(` status : ${verdict.status.toUpperCase()}`);
console.log(` detail : ${verdict.detail}`);

if (!verdict.ok) {
console.log(` fix : run 'make db-sync' to fetch the latest CI release DB for grounding.`);
process.exit(1);
}
process.exit(0);
Loading
Loading