Skip to content

feat(explain): promote Q16 write-shape tristate to production (phase 0.5, #186) - #195

Open
mobileskyfi wants to merge 5 commits into
mainfrom
explain-q16-write
Open

feat(explain): promote Q16 write-shape tristate to production (phase 0.5, #186)#195
mobileskyfi wants to merge 5 commits into
mainfrom
explain-q16-write

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Links

Change type

  • Behavior change
  • Docs/spec-only change
  • Tooling/workflow change
  • Security-sensitive change (a write-shape safety signal over untrusted editor/MCP input → fail-closed)

Notes

  • Validation run: bun run lint · bun run lint:ci · bun test (1727 pass / 0 fail) · bun run build — all green.
  • RouterOS/protocol assumptions: offline, schema-free. The vocabularies are frequency-justified against 52 command trees 7.9→7.24beta1 and frozen; the ratified rule is version-independent (decision 3 stays closed). explain stays designed in the MATRIX (no live protocol cell).

What

The next phase-0.5 vertical slice (#186): promotes the ratified write-shape tristate into src/explain/write.ts. This is the first of the two ratified amendments to reach production, and it consumes the Q6 boundary that landed in #193 — Q16 composes with Q6 rather than needing a schema of its own.

src/explain/write.ts (Q16 — structure.containsWrite)

  • containsWrite(text){ verdict: "true" | "false" | "unknown", writes, blockers, occurrences, notes }, plus occurrences(text) and classifyVerb(verb, directive) and the five frozen vocabularies.
  • Only the ratified failclosed navigation arm is promoted. The two arms that priced it (q4nav/q6split) stayed in the lab, along with the corpus scorer, the vocabulary recon, and the staleness probe.
  • This is not the execute gate. canonicalizeExecuteCommand's mode/writeShaped is untouched; containsWrite is a distinct explain-only inference with basis heuristic, exactly as the spec requires.

Why the tristate, restated at the code: no schema artifact carries mutation semantics (inspect.json tags nodes path|dir|cmd|arg; restraml's method assignment is rule-derived and circular to read back), so the table is curated — and while a 17-name table covers 93.1% of cmd nodes on 7.24beta1 and 93.8% on 7.9.2 (0.07% churn per minor version), 56 of the 225 uncovered verb names are write-shaped. A boolean would have to call /disk format-drive a non-write. The gap is standing, not staleness.

The four ratified rollup rules ship verbatim: a proven write wins outright; with no proven write any unclassifiable occurrence yields unknown, never false; statements that cannot be confirmed as navigation emit an occurrence rather than being dropped (finding 2 — fail-closed has to be enforced where input is discarded, not where it is classified); and unknown is reported with its blockers.

Two fail-closed corrections from composing with the production resolvers

Both are places where the production Q3/Q4 resolver is stricter than the lab's, so a faithful port would have left a hole. Neither invents a rule — each applies a floor the spec already ratifies.

  1. Q14 defect floor. resolveStatements refuses a structurally malformed statement and does not descend into it, so a write may hide in a body that was never walked. Reading it as harmless control flow would clear the document (:if [) do={ /ip route add … }false). It now emits a blocking defect occurrence, and a document-level segmenter/over-depth note abstains on its own.
  2. Bare-word head → ambiguous. Production menuNavPath requires a leading /, because Q4's CHR round confirmed bare-word navigation is not valid RouterOS. So a bare-word head (reset-counters under an /interface context) never reaches the unconfirmed-nav fallback and instead arrives with V4 ambiguity and no decided verb. Clearing it would be a false negative on a curated write verb — the one thing the hard threshold forbids — so an undecided boundary over a non-empty run abstains. That is Q14's rule (b) enforced at the rollup.

Net effect on the measured numbers: strictly more abstention, never less. Both moves trade precision for the pre-registered zero-false-negative requirement, which is the direction the tristate exists to allow.

Left as measured, declared not fixed

A slash-led bare path with no hyphenated segment reads as confirmed navigation and is dropped — right for /ip/firewall/filter (a directory), wrong for /system/reboot (a no-argument write). The two are the same text; only a schema separates them. This is the lab's declared overconfident bin — 3 of 352 holdout captures, reported separately and deliberately excluded from the precision denominator because the IL oracle cannot decide those cells either. It is pinned by an explicit KNOWN LIMIT test so the residual is visible rather than latent; narrowing isConfirmedNav needs a corpus re-score, not a guess.

Tests

Fixtures + anchor tests (79 total, all green):

  • 35 oracle-checked corners promoted from .scratch/explain-lab-q16-corners.ts — direct writes (incl. the four bare-word-operand shapes finding 1 named), other mutating verbs, nav-then-write, nested writes at do=/else=/:onerror in= depth, the four inert cases that must not count (string, comment, literal source={…}, write stored in a variable), six dynamic cases that must be unknown, root write vs root read (:set is a variable, /ip/route set is a device write), and three long-tail verbs that must abstain.
  • 8 branch anchors for the paths the corners do not reach: both Q14 defect classes, empty/comment-only input, rule 1 and rule 2 in isolation, confirmed navigation, and an unseen root directive.
  • The pre-registered thresholds as tests: zero false negatives on statically obvious writes, zero false positives on inert command-shaped text, and no must-abstain case reported false — each asserted over every fixture, which is strictly stronger than matching per-case verdicts.
  • Frozen vocabularies (exact set membership, non-overlap, root-table containment), the (menu, verb) dependency that makes set/import/undo/export/find mean different things at root and at a menu, rollup consistency as a property over adversarial input, order independence, determinism, never-throws, bounded depth, and the public export surface.

Left behind (measurement rig, not product)

The two non-failclosed nav arms, the corpus scorer, the vocabulary reconnaissance, the staleness/time-travel probe, and the containsWriteBoolean shape-comparison helper.

🤖 Generated with Claude Code


Review round (a17b0c6) — all 5 threads addressed and resolved

Copilot and CodeRabbit independently found the same real bug, and both were right:

  • Unanchored [$…] dynamic-form probe (both bots). It fired on any [$…] anywhere in the line, so a fully-known write was masked: /ip route add gateway=[$gw] returned unknown/0 writes instead of true/1. Now anchored to /^\s*\[\s*\$/ — a dynamic argument value does not make a curated write verb uncertain (rule 1), and an inner $gw still abstains on its own via the bracket walk. Three regression tests added.
  • Raw NUL/SOH bytes in the adversarial fixture list (Copilot). Genuine — file(1) classified the test as data, and my first check used a matcher that missed them. Replaced with explicit escapes.
  • Redundant reparsing (CodeRabbit). collect now parses each statement once and threads the result through all four boundary rules instead of re-deriving it 3–4× per statement. Behavior unchanged; the frozen fixtures and property tests cover it.
  • Misleading "SET, not a sequence" JSDoc (Copilot). Reworded: order carries no meaning, but repeats are significant (writes counts multiplicity), so the list must not be deduped.

Re-validated: bun run lint · bun run lint:ci · bun test (1730 pass / 0 fail) · bun run build — all green; CI all green including CHR smoke.

Summary by CodeRabbit

  • New Features

    • Added offline analysis to determine whether RouterOS scripts contain device-changing commands.
    • Results now distinguish confirmed writes, confirmed non-writes, and unknown cases when analysis cannot be certain.
    • Added detailed occurrence classifications and diagnostic information for ambiguous, dynamic, or malformed commands.
    • Expanded supported RouterOS vocabulary, including root directives and additional menu terms.
  • Tests

    • Added comprehensive coverage for writes, reads, malformed input, dynamic commands, navigation, comments, and nested control flow.

…0.5, #186)

Promotes the ratified `containsWrite` tristate from the throwaway lab probe
`.scratch/explain-lab-q16-write.ts` into `src/explain/write.ts`, on top of the
Q1 segmenter (#189), the Q2-Q4 path resolver (#191), and the Q6 verb/menu
boundary (#193).

Only the ratified `failclosed` navigation arm ships; the two arms that priced
it stayed in the lab. All 35 oracle-checked corners are promoted as frozen
fixtures, plus branch anchors for the ratified rollup rules, the frozen
vocabularies, the Q14 defect floor, and the pre-registered hard-0 threshold
(zero false negatives on statically obvious writes).

Two fail-closed corrections fall out of composing with the production
resolvers rather than the lab's:

- a statement the path resolver refused (Q14 structural defect) is not
  descended, so a write may hide in a body that was never walked: it now
  emits a blocking `defect` occurrence instead of being read as control flow;
- a bare-word head arrives with V4 ambiguity and no decided verb, because
  production `menuNavPath` requires a leading `/` (Q4's CHR correction that
  bare-word navigation is not valid RouterOS). Clearing it would be a false
  negative on a curated write verb, so it abstains -- Q14's rule (b).

The lab's declared `overconfident` residual (a trailing bare path clears
instead of abstaining) is carried as measured and pinned by a KNOWN-LIMIT
test rather than fixed by guess; narrowing it needs a corpus re-score.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 20:54
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds offline RouterOS write inference with curated vocabularies, occurrence classification, structural-failure handling, and fail-closed tristate results. Exports the API, documents vocabulary, and adds fixture-driven classification and robustness coverage.

Changes

Write analysis

Layer / File(s) Summary
Classification contracts and vocabularies
src/explain/write.ts, src/index.ts, GLOSSARY.txt, test/unit/explain-write.test.ts
Defines menu and root command vocabularies, public occurrence types, verb classification, glossary terms, and root-level exports.
Occurrence collection and navigation handling
src/explain/write.ts, test/fixtures/explain/write.json, test/unit/explain-write.test.ts
Collects statement and bracket occurrences, detects dynamic and defective inputs, applies navigation confirmation rules, and validates command-shape behavior.
Fail-closed verdict rollup
src/explain/write.ts, test/unit/explain-write.test.ts
Aggregates writes, blockers, and structural notes into true, false, or unknown, with rollup, defect, determinism, and robustness assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Input
  participant occurrences
  participant resolveStatements
  participant resolveDocument
  participant containsWrite
  Input->>occurrences: provide RouterOS text
  occurrences->>resolveStatements: resolve statements
  occurrences->>resolveDocument: resolve brackets and document notes
  resolveStatements-->>occurrences: classified statements
  resolveDocument-->>occurrences: classified bracket content and notes
  occurrences-->>containsWrite: return occurrences and notes
  containsWrite-->>Input: return write analysis
Loading

Possibly related issues

Possibly related PRs

  • tikoci/centrs#187 — Defines the fail-closed tristate and diagnostics semantics implemented by this PR.
  • tikoci/centrs#191 — Provides the statement and document resolution outputs consumed by write.ts.
  • tikoci/centrs#193 — Provides the verb/menu boundary used by classifyVerb and containsWrite.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: promoting the Q16 write-shape tristate to production.
Description check ✅ Passed The description follows the required template and includes links, change type, and notes with validation and assumptions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch explain-q16-write

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Promotes the Q16 “write-shape” tristate inference into the production src/explain surface, including a new containsWrite() analysis API plus fixtures/tests that pin the ratified fail-closed rules.

Changes:

  • Added src/explain/write.ts implementing containsWrite() / occurrences() and the frozen vocabularies used for classification.
  • Exported the new explain/write API from src/index.ts (with occurrences renamed to explainOccurrences at the library root).
  • Added Q16 fixtures and unit tests to lock in the tristate behavior and safety thresholds; updated glossary terms used by the new tests/docs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/explain/write.ts New production implementation of Q16 write-shape tristate inference and rollup.
src/index.ts Re-exports the new explain/write APIs and types from the library root.
test/fixtures/explain/write.json Adds Q16 fixture corpus (corners + branch anchors) consumed by unit tests.
test/unit/explain-write.test.ts Adds comprehensive unit coverage for Q16 invariants, thresholds, and export surface.
GLOSSARY.txt Adds new project vocabulary tokens referenced by the promoted explain/write work.

Comment thread src/explain/write.ts Outdated
Comment thread src/explain/write.ts Outdated
Comment thread test/unit/explain-write.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/explain/write.ts`:
- Around line 291-312: The navigable-statement flow reparses identical text
multiple times. Compute describeStatement(t) once in collect and update
isCommandShaped, isConfirmedNav, classifyStatement, and classifyUnconfirmedNav
to accept and reuse that parsed result, preserving their existing classification
behavior.
- Around line 195-204: Anchor the bracket-variable pattern in DYNAMIC_FORMS so
isDynamicForm only treats a statement or bracket-inner text beginning with [$…]
as dynamic, rather than matching argument values mid-string. Preserve the
existing dynamic detection for head-position variable/function calls, and add a
regression case for containsWrite("/ip route add gateway=[$gw]") expecting a
true verdict with one write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c417e30-107c-447f-8239-ec122f14d119

📥 Commits

Reviewing files that changed from the base of the PR and between 8df4c6a and e04e1bc.

📒 Files selected for processing (5)
  • GLOSSARY.txt
  • src/explain/write.ts
  • src/index.ts
  • test/fixtures/explain/write.json
  • test/unit/explain-write.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: CHR smoke (stable)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (4)
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Run bun run lint && bun run test && bun run build before finishing code changes.
Never commit secrets, real router credentials, private CDB/Dude databases, or sensitive packet captures.
Do not make generated output the hand-edited source of truth.
Do not add new documentation for status, plans, or roadmaps; update the constitution, matrix, or relevant per-command files instead.
Prefer rosetta MCP tools for RouterOS facts before using web search.
Write code or tests only after reading the constitution, matrix, and relevant command specification; do not write additional prose during implementation.

Files:

  • GLOSSARY.txt
  • src/index.ts
  • test/fixtures/explain/write.json
  • test/unit/explain-write.test.ts
  • src/explain/write.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (src/AGENTS.md)

src/**/*.{ts,tsx}: Use Bun-native TypeScript and Web APIs where possible
Errors must be actionable for humans and agents, with next-step guidance when a dependency, protocol, credential, or validation source is missing

src/**/*.{ts,tsx}: Use Bun-first TypeScript workflows for source code and run the required lint, test, and build verification commands before finishing changes.
Do not silently fall back to another protocol when the caller pins --via.
Treat credential handling, RouterOS write execution, proxy listeners, and local discovery data as security-sensitive surfaces; follow SECURITY.md and the security-quality instructions.
Do not disable validation to make a test pass; validation is part of the product.

Files:

  • src/index.ts
  • src/explain/write.ts
test/fixtures/**/*

📄 CodeRabbit inference engine (test/AGENTS.md)

Keep fixtures under test/fixtures/ with clear source/provenance notes.

Files:

  • test/fixtures/explain/write.json
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Unit tests passing alone do not establish completion; transport- or RouterOS-touching tests require bun run test:integration.

Files:

  • test/unit/explain-write.test.ts
🔇 Additional comments (5)
src/explain/write.ts (1)

1-186: LGTM! Vocabularies, classifyVerb, and the containsWrite rollup are internally consistent — cross-checked WRITE_VERBS/READ_VERBS/ROOT_WRITE/ROOT_DYNAMIC/ROOT_CMDS sizes/contents and the rollup rules against the accompanying fixtures/tests and found no discrepancies.

Also applies to: 213-269, 469-531

GLOSSARY.txt (1)

530-561: LGTM!

test/unit/explain-write.test.ts (1)

1-420: LGTM! Assertions align with the implementation's documented rollup/classification rules.

src/index.ts (1)

346-361: LGTM!

test/fixtures/explain/write.json (1)

1-317: LGTM! Fixture provenance note satisfies the test/fixtures/**/* requirement. As per path instructions, "Keep fixtures under test/fixtures/ with clear source/provenance notes."

Source: Path instructions

Comment thread src/explain/write.ts Outdated
Comment thread src/explain/write.ts Outdated
mobileskyfi and others added 2 commits July 24, 2026 14:07
…p NUL bytes

- Anchor the `[$…]` dynamic-form regex (Copilot + CodeRabbit, both correct).
  Unanchored it fired on any `[$…]` anywhere in the line, so a fully-known
  write was masked as `dynamic`: `/ip route add gateway=[$gw]` returned
  `unknown`/0 writes instead of `true`/1. A dynamic argument VALUE does not
  make a curated write verb uncertain (rule 1), and the inner `$gw` still
  abstains on its own through the bracket walk. Three regression tests added.
- Replace the raw NUL/SOH bytes in the adversarial fixture list with escapes
  (Copilot). They were real -- `file(1)` classified the test as `data` -- and
  survived a first grep check that used the wrong matcher.
- Parse each statement once and thread the result through the boundary rules
  instead of re-deriving it three or four times per statement (CodeRabbit).
- Reword the occurrences JSDoc: order carries no meaning, but the list is not
  a set and repeats are significant, so it must not be deduped (Copilot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread test/unit/explain-write.test.ts Outdated

@mobileskyfi mobileskyfi left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review pass completed at 4f447b5.

I fixed three clear Q16 composition defects in that commit:

  • Stored-script detection now reads the leading token run. The previous raw substring probe masked a definite write when an argument string contained /system script run, while also dropping the valid mixed spelling /system/script run backup as clean navigation.
  • Non-structural path-resolution outcomes ([find], expression/string heads, variable calls) no longer become fabricated defect occurrences; each now follows the existing dynamic / no-verb contract.
  • Bracketed bare paths retain the Q6 ambiguous floor, so :put [/system/reboot] cannot clear as read-only.

Focused coverage is 90/90. The normal pre-push gate passed: lint + CI lint, bun test (1,738 pass / 0 fail / 35 skipped), and build.

One P1 remains inline: the explicitly pinned top-level /system/reboot false negative conflicts with the constitution and the hard-zero Q16 safety threshold. I left it unpatched because changing the navigation arm requires the stated corpus re-score; that is major work, not a local predicate tweak.

mobileskyfi and others added 2 commits July 24, 2026 14:43
…n leaks

Addresses the open P1 on #195 and reviews 4f447b5.

Codex's three fixes are correct and kept: the run-based stored-script probe
(a raw substring probe masked writes whose ARGUMENT contained the text, and
missed mixed separator spellings), the narrowed Q14 defect branch, and the
bracket `ambiguous` verdict that made brackets consistent with statements.

Three changes on top.

1. P1 — a dangling bare path now abstains. `isConfirmedNav` decides at
   statement scale, so a lone or trailing `/system/reboot` was dropped as
   navigation and cleared the document to `false`: a false negative on a
   statically obvious write, reproducing inside the tristate the exact failure
   the tristate was ratified to prevent. Position is the schema-free
   confirmation — navigating and then ending the script is a no-op, while a
   no-argument command is meaningful. Measured over the frozen 911-script
   corpus this moves ZERO documents, so the ratified navigation arm's
   abstention is unchanged (44.0% dev / 45.2% holdout). The fuller rule
   (abstain unless a later RELATIVE statement consumes the context) was also
   measured and is declared as a priced residual, not taken blind: +1.4pp dev
   / +1.9pp holdout across 14 documents, all 14 genuine directories.

2. Narrowing the defect branch to a "structural defect:" prefix failed OPEN
   for two of pathresolve's four statement refusal reasons. `runTokens` stops
   at the first non-bare segment, so `/ip/$menu/remove 0` and
   `/ip/$m/add address=1.2.3.4` yield an EMPTY run and are refused with "no
   leading path token" — the same reason a harmless `"a string"` gets — and
   both returned `false`. Now a `/`-led statement whose path could not be read
   abstains, and the refusal-reason check is an ALLOW list so an unrecognized
   or reworded reason blocks instead of clearing.

3. Corpus accounting for 2: 4 documents move `false` -> `unknown`, all pasted
   CLI transcripts (`/interface/lte> monitor lte1,lte2`), where the previous
   answer was a confident "no write" about unparseable text.

The KNOWN LIMIT test is replaced by real assertions plus the declared residual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewing b9dec39 found its two statement-side fixes had an exact twin in the
bracket walk that was never closed. Both guards were applied to statements
only, so the same text abstained or cleared depending on whether it sat inside
brackets:

    /ip/$menu/remove 0          -> unknown  (ambiguous)
    :put [/ip/$menu/remove 0]   -> FALSE    (no-verb)

A confident "no write" about a statement that plainly names a curated write
verb is a false negative on a statically obvious write — the one outcome Q16's
hard-0 threshold forbids, reproduced inside the tristate for the second time.
Finding 2 says fail-closed is enforced where input is DISCARDED, and a bracket
discards it just as thoroughly as a statement does.

Two changes, mirroring the statement branch exactly.

1. The Q14 defect floor now runs over bracket resolutions. Brackets carry
   their own refusal vocabulary, so this is not redundant with the statement
   check: `variable path segment` and `looks like a pasted CLI prompt, not a
   substitution` are reachable here and neither is on the ALLOW list, so both
   block instead of clearing.

2. The bracket `ambiguous` test becomes `isPathShapedButUnread` rather than a
   bare `run.length > 0`. Independently load-bearing: `[/ip/"foo"/remove]` and
   `[/ip/[find]/remove]` yield an EMPTY run with NO refusal reason, so rule 1
   does not see them and only the path-shaped guard catches them.

Priced over the frozen 911-script corpus with the same harness as b9dec39:
+0.8pp abstention on both splits (44.0 -> 44.8 dev, 45.2 -> 46.0 holdout).
All 7 moved documents go `false` -> `unknown`; ZERO move off `true`, so no
precision is lost on decided writes. All 7 are pasted CLI transcripts whose
`[user@device]` prompt parses as a bracket — and two of them
(`/ip/socks/set enabled=yes`, `/system identity set name=…`) hide a real write
behind that prompt, so those were genuine false negatives, not over-abstention.

The 35 frozen oracle corners are unaffected. Five regression tests added,
including the corpus transcript shape and an anchor on the bracket refusal
vocabulary, which differs from the statement one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Reviewed the current state of this branch, including the two review-response commits (a17b0c6, 4f447b5) and b9dec39. All six review threads are resolved and their fixes hold up. One further defect found and fixed in d23fb4a.

The bracket walk was missing both fail-closed guards

b9dec39 fixed two fail-open leaks in the statement walk. Both had an exact twin in the bracket walk that was never closed, so the same text got opposite answers depending on whether it sat inside brackets:

/ip/$menu/remove 0          -> unknown  (ambiguous)   correct
:put [/ip/$menu/remove 0]   -> FALSE    (no-verb)     false negative

A confident "no write" about a statement that plainly names a curated write verb is exactly what the Q16 hard-0 threshold forbids. Finding 2 says fail-closed is enforced where input is discarded — a bracket discards it just as thoroughly as a statement does.

Two changes, mirroring the statement branch:

  1. The Q14 defect floor now runs over bracket resolutions. Not redundant with the statement check: brackets have their own refusal vocabulary. variable path segment and looks like a pasted CLI prompt, not a substitution are reachable here, neither is on the ALLOW list, so both block.
  2. The bracket ambiguous test is now isPathShapedButUnread rather than a bare run.length > 0. Independently load-bearing — [/ip/"foo"/remove] and [/ip/[find]/remove] yield an empty run with no refusal reason, so rule 1 never sees them.

Priced on the frozen corpus

Same harness and discipline as b9dec39, over all 911 documents:

split before after
dev 44.0% 44.8%
holdout 45.2% 46.0%

+0.8pp on both splits, 7 documents moved. All 7 go false -> unknown; zero move off true, so no precision is lost on decided writes.

All 7 are pasted CLI transcripts where the [user@device] prompt parses as a bracket — and two of them hide a real write behind that prompt:

  • [skyfi@M171-Yizi] > /ip/socks/set enabled=yes
  • [me@Mikrotik] > /system identity set name="\E2\9D\A4"

The statement walk reads those lines as starting with [, so the write is never classified and the document previously cleared to a confident false. Those were genuine false negatives, not over-abstention — which makes this a strict improvement on the hard-0 threshold rather than only a cost.

The 35 frozen oracle corners are unaffected. Five regression tests added, including the corpus transcript shape and an anchor on the bracket refusal vocabulary (which differs from the statement one, so drift in either fails closed independently).

Not merging — leaving this for review.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 32 minutes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@GLOSSARY.txt`:
- Around line 548-561: Add the missing toip6 entry to the ROOT_CMDS dictionary
block in GLOSSARY.txt, alongside the existing command names, so the glossary
matches the ROOT_CMDS declaration in src/explain/write.ts.

In `@src/explain/write.ts`:
- Around line 365-375: Update collect to compute the last non-empty statement
index once before its loop, then pass that value to isDanglingBarePath at the
existing call site. Change isDanglingBarePath to compare the current index with
the precomputed lastNonEmpty value and remove its per-statement tail scan,
preserving the existing described.run empty check.

In `@test/unit/explain-write.test.ts`:
- Around line 366-375: Update the “the root tables are exactly the measured
sets” test to assert the sorted contents of ROOT_CMDS, not only its size.
Preserve the existing ROOT_WRITE and ROOT_DYNAMIC assertions, and enumerate all
56 expected command names so vocabulary substitutions are detected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 692c386f-0af6-4178-8c01-96376a12860b

📥 Commits

Reviewing files that changed from the base of the PR and between 8df4c6a and d23fb4a.

📒 Files selected for processing (5)
  • GLOSSARY.txt
  • src/explain/write.ts
  • src/index.ts
  • test/fixtures/explain/write.json
  • test/unit/explain-write.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: Run bun run lint && bun run test && bun run build before finishing code changes.
Never commit secrets, real router credentials, private CDB/Dude databases, or sensitive packet captures.
Do not make generated output the hand-edited source of truth.
Do not add new documentation for status, plans, or roadmaps; update the constitution, matrix, or relevant per-command files instead.
Prefer rosetta MCP tools for RouterOS facts before using web search.
Write code or tests only after reading the constitution, matrix, and relevant command specification; do not write additional prose during implementation.

Files:

  • GLOSSARY.txt
  • src/index.ts
  • test/fixtures/explain/write.json
  • test/unit/explain-write.test.ts
  • src/explain/write.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (src/AGENTS.md)

src/**/*.{ts,tsx}: Use Bun-native TypeScript and Web APIs where possible
Errors must be actionable for humans and agents, with next-step guidance when a dependency, protocol, credential, or validation source is missing

src/**/*.{ts,tsx}: Use Bun-first TypeScript workflows for source code and run the required lint, test, and build verification commands before finishing changes.
Do not silently fall back to another protocol when the caller pins --via.
Treat credential handling, RouterOS write execution, proxy listeners, and local discovery data as security-sensitive surfaces; follow SECURITY.md and the security-quality instructions.
Do not disable validation to make a test pass; validation is part of the product.

Files:

  • src/index.ts
  • src/explain/write.ts
test/fixtures/**/*

📄 CodeRabbit inference engine (test/AGENTS.md)

Keep fixtures under test/fixtures/ with clear source/provenance notes.

Files:

  • test/fixtures/explain/write.json
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Unit tests passing alone do not establish completion; transport- or RouterOS-touching tests require bun run test:integration.

Files:

  • test/unit/explain-write.test.ts
🔇 Additional comments (8)
src/explain/write.ts (4)

86-200: LGTM!


229-238: LGTM!


494-601: LGTM!


641-668: LGTM!

src/index.ts (1)

346-361: LGTM!

test/unit/explain-write.test.ts (2)

390-562: LGTM!


564-655: LGTM!

test/fixtures/explain/write.json (1)

1-330: LGTM!

Comment thread GLOSSARY.txt
Comment on lines +548 to +561
failclosed
notadirective
jobname
rndnum
rndstr
tobool
tocrlf
toid
toip
tolf
tonsec
tonum
totime
lte

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H -t f 'cspell*' -x cat {}
rg -n 'toip6' --hidden

Repository: tikoci/centrs

Length of output: 1525


🏁 Script executed:

#!/bin/bash
set -e
printf '--- candidate files ---\n'
fd -H -t f 'cspell|write|GLOSSARY|composer|package|bun' -x sh -c 'echo "FILE:$0"; wc -l "$0"' {}
printf '\n--- toip6 occurrences ---\n'
rg -n -C 2 'toip6|ROOT_CMDS|toid|toip|tolf|tonsec' . --hidden || true
printf '\n--- cspell config relevant sections ---\n'
if [ -f cspell.json ]; then jq '.dictionaryDefinitions, .dictionaries, .ignoreWords, .ignorePaths' cspell.json; fi
if [ -f package.json ]; then jq '.scripts' package.json; fi

Repository: tikoci/centrs

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -H -t f 'cspell|write|GLOSSARY|composer|package|bun' -x sh -c 'echo "FILE:$0"; wc -l "$0"' {}
printf '%s\n' ''
printf '%s\n' '--- toip6 occurrences ---'
rg -n -C 2 'toip6|ROOT_CMDS|toid|toip|tolf|tonsec' . --hidden || true
printf '%s\n' ''
printf '%s\n' '--- cspell config relevant sections ---'
if [ -f cspell.json ]; then
  jq '.dictionaryDefinitions, .dictionaries, .ignoreWords, .ignorePaths' cspell.json || true
fi
if [ -f package.json ]; then
  jq '.scripts' package.json || true
fi

Repository: tikoci/centrs

Length of output: 8095


Add toip6 to the ROOT_CMDS glossary.

src/explain/write.ts now declares toip6 in ROOT_CMDS, but GLOSSARY.txt only lists the previous ROOT_CMDS entries. CSpell uses GLOSSARY.txt as project-words, so add toip6 to the ROOT_CMDS dictionary block to keep bun run lint:ci from failing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GLOSSARY.txt` around lines 548 - 561, Add the missing toip6 entry to the
ROOT_CMDS dictionary block in GLOSSARY.txt, alongside the existing command
names, so the glossary matches the ROOT_CMDS declaration in
src/explain/write.ts.

Comment thread src/explain/write.ts
Comment on lines +365 to +375
function isDanglingBarePath(
described: Described,
statements: readonly StatementResolution[],
index: number,
): boolean {
if (described.run.length === 0) return false;
for (let j = index + 1; j < statements.length; j++)
if ((statements[j] as StatementResolution).text.trim().length > 0)
return false;
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

isDanglingBarePath rescans the tail per statement — O(n²) on nav-heavy input.

The trailing-blank scan is recomputed for every confirmed-nav statement. Compute the index of the last non-empty statement once in collect and compare.

♻️ Proposed refactor
 function isDanglingBarePath(
 	described: Described,
-	statements: readonly StatementResolution[],
+	lastNonEmpty: number,
 	index: number,
 ): boolean {
 	if (described.run.length === 0) return false;
-	for (let j = index + 1; j < statements.length; j++)
-		if ((statements[j] as StatementResolution).text.trim().length > 0)
-			return false;
-	return true;
+	return index >= lastNonEmpty;
 }

In collect, derive lastNonEmpty once before the loop and pass it at Line 543.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/explain/write.ts` around lines 365 - 375, Update collect to compute the
last non-empty statement index once before its loop, then pass that value to
isDanglingBarePath at the existing call site. Change isDanglingBarePath to
compare the current index with the precomputed lastNonEmpty value and remove its
per-statement tail scan, preserving the existing described.run empty check.

Comment on lines +366 to +375
test("the root tables are exactly the measured sets", () => {
expect([...ROOT_WRITE].sort()).toEqual([
"import",
"password",
"redo",
"undo",
]);
expect([...ROOT_DYNAMIC].sort()).toEqual(["execute", "parse"]);
expect(ROOT_CMDS.size).toBe(56);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin ROOT_CMDS contents, not just its size.

expect(ROOT_CMDS.size).toBe(56) passes if a name is swapped rather than added/removed — weaker than the sibling tables, which assert sorted contents. The stated purpose is freezing the vocabulary against tuning.

♻️ Proposed change
-		expect(ROOT_CMDS.size).toBe(56);
+		expect([...ROOT_CMDS].sort()).toEqual([
+			"beep", "break", "continue", "convert", "delay", "deserialize", "do",
+			"error", "execute", "exit", "export", "find", "for", "foreach",
+			"global", "grep", "if", "import", "jobname", "len", "local", "lock",
+			"nothing", "onerror", "parse", "password", "pick", "ping", "put",
+			"quit", "range", "recursive-print", "redo", "resolve", "retry",
+			"return", "rndnum", "rndstr", "serialize", "set", "time", "timestamp",
+			"toarray", "tobool", "tocrlf", "toid", "toip", "toip6", "tolf",
+			"tonsec", "tonum", "tostr", "totime", "typeof", "undo", "while",
+		]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("the root tables are exactly the measured sets", () => {
expect([...ROOT_WRITE].sort()).toEqual([
"import",
"password",
"redo",
"undo",
]);
expect([...ROOT_DYNAMIC].sort()).toEqual(["execute", "parse"]);
expect(ROOT_CMDS.size).toBe(56);
});
test("the root tables are exactly the measured sets", () => {
expect([...ROOT_WRITE].sort()).toEqual([
"import",
"password",
"redo",
"undo",
]);
expect([...ROOT_DYNAMIC].sort()).toEqual(["execute", "parse"]);
expect([...ROOT_CMDS].sort()).toEqual([
"beep", "break", "continue", "convert", "delay", "deserialize", "do",
"error", "execute", "exit", "export", "find", "for", "foreach",
"global", "grep", "if", "import", "jobname", "len", "local", "lock",
"nothing", "onerror", "parse", "password", "pick", "ping", "put",
"quit", "range", "recursive-print", "redo", "resolve", "retry",
"return", "rndnum", "rndstr", "serialize", "set", "time", "timestamp",
"toarray", "tobool", "tocrlf", "toid", "toip", "toip6", "tolf",
"tonsec", "tonum", "tostr", "totime", "typeof", "undo", "while",
]);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/explain-write.test.ts` around lines 366 - 375, Update the “the root
tables are exactly the measured sets” test to assert the sorted contents of
ROOT_CMDS, not only its size. Preserve the existing ROOT_WRITE and ROOT_DYNAMIC
assertions, and enumerate all 56 expected command names so vocabulary
substitutions are detected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants