Skip to content

feat(explain): ground the operator axis on the device (#255) — step 1 - #287

Merged
mobileskyfi merged 5 commits into
mainfrom
agent/255-operator-axis-grounding
Aug 12, 2026
Merged

feat(explain): ground the operator axis on the device (#255) — step 1#287
mobileskyfi merged 5 commits into
mainfrom
agent/255-operator-axis-grounding

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Grounding-only step for #255 — sweep the RouterOS operator surface rather than transcribe the manual (same shape as #252).

What this does

  • Adds offline IL head census (scripts/explain-operator-census.ts, no CHR) as candidate generator
  • Sweeps manual ∪ plausible non-operators ∪ census heads on CHR 7.23.3 stable + 7.24rc4 testing via :parse IL (naming oracle) + highlight structure-vs-word only (scripts/probes/explain-operator-sweep.ts)
  • Slices reviewed fixture → src/explain/operators.ts + test/fixtures/explain/operators.json (scripts/explain-operator-slice.ts)
  • Generates commands/explain/README.md operator table; gates via explain:operator-census:check (corpus job) + explain:operator-readme:check (lint:ci)
  • 26 operators, 14 precedence levels (576 ordered pairs, 2*tighter+(same-1) strict weak order, 0 dropped), 3-way associativity (9 variadic flattened), plus !/any prefix-only

Not in this PR
No span emission, no new ExplainSpanClass, no defect rule — #264 B2 (operator fill) + B4 (centrs→highlight projection) will read src/explain/operators.ts. explainCommand output byte-identical to main on six baselines.

Device findings

  • not/xor/mod/is/div/band/bor/shl/shr/eq/ne are not operators ((1 not 2)( 1 $not 2) juxtaposition)
  • ...+$., ///+$/, <>(< 1 (> 2)) re-lexed
  • $/[/] never head a node (substitution axis)
  • any is an undocumented prefix operator; &&/|| are alias spellings of and/or
  • (> x) arity 1 deferred expression vs (2 > 1) arity 2 comparison — arity is the discriminator; :typeof op vs array only at runtime; <%% binds $0 vs do={} $1
  • Identical on 7.23.3 + 7.24rc4 except one runtime row: ({2;1} > {1;2;3}) errors on stable, true on testing

See draft review comments for nits and probe gaps.

Closes #255 step 1 (grounding).

Summary by CodeRabbit

  • New Features

    • Added a comprehensive RouterOS operator reference covering recognized operators, aliases, precedence, associativity, arity, syntax exceptions, and version differences.
    • Added operator classification and lookup support, including lowered spellings and non-operator forms.
    • Added device-derived operator data and behavior results for 26 operators.
  • Documentation

    • Expanded the explain documentation with generated operator tables and rejected-spelling guidance.
    • Added glossary definitions for operator-axis terminology.
  • Bug Fixes

    • Added automated checks to detect changes in operator census data and documentation output.

#255 step 1: sweep the RouterOS operator surface rather than transcribe the
manual's list, the way #252 had to redo the escape set. Grounding only — no
span, no new class, no defect rule. `explainCommand` output is byte-identical
to main on the six baseline inputs.

Three tools, three links, each with its own gate:

- `explain:operator-census` mines IL heads from the 2,739 committed corpus
  `:parse` captures (no CHR). It is a CANDIDATE GENERATOR only: RouterOS IL is
  a debug rendering with no string quoting, so string content lands in head
  position, and the census reports shape rather than filtering.
- `explain:probe:operators` sweeps the manual's list, plausible non-operators
  and the census heads on CHR 7.23.3 stable and 7.24rc4 testing, with `:parse`
  IL as the naming oracle and `highlight` for structure-vs-word only.
- `explain:operator-slice` cuts the reviewed fixture; `src/explain/operators.ts`
  is held to it by `explain-operators.test.ts`, and the README table is
  generated from the table and gated in `lint:ci`.

What the device says that the manual does not:

- `not`, `xor`, `mod`, `is`, `div`, `band`, `bor`, `shl`, `shr`, `eq`, `ne` are
  NOT operators. Inside parens a bare word is a variable reference, so
  `(1 not 2)` parses — as `(  1 $not 2)`, an unnamed juxtaposition node. "It
  parsed" is not evidence of an operator, which the `(1 zzz 2)` control proved
  by failing the probe's first verdict rule.
- `..` is `.` plus a variable named `.`; `//` likewise; `<>` is `(< 1 (> 2))`.
- `$`, `[`, `]` never head a node — they are on the substitution axis.
- `any` is an undocumented prefix operator.
- `&&`/`||` are spellings of the `and`/`or` nodes.

Precedence is measured, not transcribed: over all 576 ordered pairs the outer
counts land exactly on `2*(tighter) + (same level)` with 0 dropped, giving 14
levels. Associativity has three answers, not two — nine operators are variadic
because the device flattens them.

`highlight` cannot give an operator boundary: `syntax-meta` is the residual
structure class (it also covers quotes, braces, brackets, whitespace) and
adjacent runs are merged. #255's framing of it as the operator class is
corrected in the README.

The `(>…)`/`<%%` axis is covered: arity is the only thing separating the
deferred-expression `(> x)` from the comparison `(2 > 1)`, `[:typeof]` of a
deferred command body is `op` while a deferred array is `array`, and `<%%`
binds positionals from `$0` where a `do={…}` function binds from `$1`.

Identical on both versions except one runtime row: array comparison with `>`
errors on 7.23.3 and evaluates on 7.24rc4.

Emission is out of scope and stays #264 B2 (operator fill) and B4 (the
`centrs → highlight` projection); both read this table.

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

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 762f8cac-ef95-4399-8b0b-f93b1a86e8b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a device-grounded RouterOS operator registry. It introduces CHR probes, corpus census tooling, committed fixture data, unit tests, generated operator documentation, and CI drift checks.

Changes

RouterOS operator surface

Layer / File(s) Summary
Device operator sweep
scripts/probes/explain-operator-sweep.ts, scripts/probes/AGENTS.md
The probe collects IL, highlight, precedence, associativity, syntax-axis, and runtime data from RouterOS versions.
Corpus census and drift check
scripts/explain-operator-census.ts
The CLI parses corpus IL, aggregates operator-head statistics, renders reports, and compares fresh results with the committed census data.
Operator registry and fixture generation
src/explain/operators.ts, scripts/explain-operator-slice.ts
The registry defines operators, aliases, lowered spellings, non-operators, arities, precedence, associativity, and lookup helpers. The slice CLI generates fixture data and README tables.
Pinned fixture and unit validation
test/fixtures/explain/operators.json, test/unit/explain-operator-probe.test.ts, test/unit/explain-operators.test.ts
The fixture stores probe and census evidence. Tests validate parsing, classification, metadata, resolution, precedence, arity, and runtime behavior.
Documentation and CI integration
commands/explain/README.md, GLOSSARY.txt, package.json, .github/workflows/ci.yaml
Documentation describes the operator surface. Package scripts and CI validate generated README content and operator census drift.

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

Sequence Diagram(s)

sequenceDiagram
  participant OperatorSweep
  participant RouterOS_CHR
  participant OperatorSlice
  participant OperatorFixture
  OperatorSweep->>RouterOS_CHR: Send parse, highlight, and runtime probes
  RouterOS_CHR-->>OperatorSweep: Return device captures
  OperatorSweep->>OperatorSlice: Provide versioned capture JSON
  OperatorSlice->>OperatorFixture: Generate operator metadata and fixture data
Loading

Possibly related PRs

  • tikoci/centrs#184: Extends the existing explain operator-analysis documentation and implementation.
  • tikoci/centrs#265: Uses the shared corpus census and CI validation workflow.
  • tikoci/centrs#267: Uses the generated documentation and fixture-drift validation pattern.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the grounding work and exclusions, but it omits the template's Links, Change type, and Notes sections. Add the required template sections, link #255, select the change type, and record the validation run and RouterOS assumptions.
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the device-grounded operator-axis work for issue #255 and indicates that this is step 1.
Linked Issues check ✅ Passed The PR fulfills the independent grounding scope in [#255] through device sweeps, census data, reviewed fixtures, operator metadata, and generated documentation.
Out of Scope Changes check ✅ Passed The changes remain within [#255] step 1 and support operator discovery, fixture generation, validation, and documentation without adding span emission or defect rules.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/255-operator-axis-grounding

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.

@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.

Muse Review

Review of #255 grounding — no blockers, 5 follow-up nits/gaps. Grounding method (IL oracle + zzz control + strict weak order) is solid. See inline comments.

Comment thread src/explain/operators.ts
Comment thread scripts/explain-operator-slice.ts
Comment thread scripts/explain-operator-slice.ts Outdated
Comment thread scripts/probes/explain-operator-sweep.ts
Comment thread test/fixtures/explain/operators.json
@mobileskyfi

mobileskyfi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Muse Review

Follow-up probes:

  • Long-term 7.21.5 CHR sweep — skipped deliberately (corpus covers 7.20.8/7.22.1/7.23rc1 offline), but a one-run on long-term would close the “long-term-only operator” gap cheaply.
  • Unary precedence!/any left as null (never in binary pair). Pairs like (!1 = 2) vs !(1 = 2) would give a level if one exists, or confirm prefix-only.
  • Dot spacing matrix — you cover (1.2)→IP and (.1)→time but not 1. 2 / 1 .2 / 1 . 2. Six inputs would harden tokenizer note.
  • Deferred value typingop type not in explain/Q12: value-type axis — shape hints, observed types, schema types (three facts, three provenances) #225 V1 lexicon. (>[:return 1]):typeof op vs (>{"a"=1})array both parse as > arity 1; <%% binds $0 vs do={} $1. Recommend filing as values issue, not lexer.
  • Stale capture.scratch/explain-255-operator-sweep-7.23.3 (stable).json (630K, no residualVariable field) is stale vs the two 314K captures used for the fixture. Remove to avoid next-agent confusion.

Overall: lint:ci + test (2691 pass) + build green, census/readme gates green, explainCommand unchanged as claimed. Ready for review.

…/! tighter than all binaries)

any is prefix arity 1, :typeof always bool, false only for nil/nothing
(the :if (any $x) idiom). Infix 'any' is juxtaposition, not an operator.
Proved on 7.23.3 + 7.24rc4 + 7.21.5 long-term and corpus 7.20.8 (any|7.20.8:2)
that !/any (and unary ~/-/>) bind tighter than every binary including ->/ <%%.

- any: 9 new runtime rows (undefined-var, defined-var, nothing, true, false,
  and/or with nothing, juxt, concat) grounded on both CHR versions; fixture
  runtime 8→17, versionDifferences still 1 (array-compare).
- probe: RUNTIME extended with those 9.
- operators.ts: doc for any nil-check and unary level 15 note.
- README: any bullet now mentions nil-check + juxt/concat + 7.20.8.
- test: two new cases for any nil-check and since-7.20.8, plus headVersions type.

Results unchanged: 26 operators, 14 binary levels, 0 dropped.
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Follow-up for any pushed as 1fea042:

  • any is a prefix nil-check (:typeof always bool, false only for nil/nothing — the :if (any $x) idiom). Covers your snippet plus true any false (juxt true + truetrue), 1 any [:nothing] (juxt 1+falsefalse), 1 . any [:nothing] (concat 1false), and and/or with any.
  • Proved on 7.23.3 + 7.24rc4 (identical) and 7.21.5 long-term; corpus shows any|7.20.8:2 so back to baseline.
  • !/any (and unary ~/-/>) bind tighter than every binary including ->/<%% — every U 1 B 2 / 1 B U 2 parses with B outermost. Documented as level 15 right-assoc in src/explain/operators.ts (binary levels stay 1–14, !/any keep null until split field lands).
  • Fixture runtime 8→17, versionDifferences still 1, tests 35 pass, lint/build green.

No operator count/precedence change — grounding-only clarifies any for future values work.

@mobileskyfi
mobileskyfi marked this pull request as ready for review August 12, 2026 19:07
Copilot AI lite review requested due to automatic review settings August 12, 2026 19:07

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

Adds a device-grounded operator inventory for RouterOS scripting (issue #255 step 1), including tooling to (a) generate candidate operator spellings from an offline IL-head corpus census, (b) sweep those candidates on CHR via :parse + highlight, and (c) slice the reviewed results into a stable in-repo table used by docs and future token emission work.

Changes:

  • Introduces src/explain/operators.ts as the durable, device-derived operator table (operators + lowered spellings + grounded non-operators).
  • Adds probe + slice + census scripts and wires new corpus/doc gates into CI and lint:ci.
  • Adds fixtures and unit tests to lock the table to the device sweep output and keep generated README content in sync.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/unit/explain-operators.test.ts Unit assertions that the operator table matches the committed sweep fixture and preserves key grounded facts (e.g., not not-operator, > arity split).
test/unit/explain-operator-probe.test.ts Unit tests for the pure helper functions used by the probe/census/sweep logic (verdicting, marker splitting, IL reading, quoting, run coalescing).
test/fixtures/explain/operators.json Committed slice of corpus census + CHR sweep results that backs src/explain/operators.ts and tests.
src/explain/operators.ts New durable operator surface table (operators, lowered spellings, grounded complement) with accessors.
scripts/probes/explain-operator-sweep.ts CHR probe that sweeps candidates via :parse IL + highlight, computes precedence/associativity, and emits captures.
scripts/probes/AGENTS.md Documents the new operator sweep probe and its durable outputs.
scripts/explain-operator-slice.ts Reduces raw sweep captures into the committed fixture and generates/gates the README operator tables.
scripts/explain-operator-census.ts Offline corpus census to enumerate IL heads (candidate generator) and gate the committed corpus block in the operators fixture.
package.json Adds operator census/slice/probe/readme scripts and adds the operator README check to lint:ci.
GLOSSARY.txt Adds operator-axis vocabulary terms used in docs/tests.
commands/explain/README.md Adds operator-surface documentation and includes the generated operator tables gated by explain:operator-readme:check.
.github/workflows/ci.yaml Extends the corpus job to run and gate the operator census (and publishes it in the job summary).
Suppressed comments (1)

commands/explain/README.md:1148

  • In the generated “Spellings the device reads as something else” table, || is rendered as `\|\|`, which displays/copies with backslashes. This is generated output; adjust the generator so pipe-containing spellings render as || while keeping the table cell valid.
| -------- | -------- | ---- |
| `&&` | `(<%% (and 1 2) )` | alias |
| `\|\|` | `(<%% (or 1 2) )` | alias |
| `<>` | `(<%% (< 1 (> 2)) )` | re-lexed |

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/probes/explain-operator-sweep.ts
Comment thread scripts/probes/explain-operator-sweep.ts
Comment thread scripts/explain-operator-slice.ts Outdated
Comment thread commands/explain/README.md

@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: 10

🤖 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 `@scripts/explain-operator-census.ts`:
- Around line 305-314: Escape pipe characters in the head value before
interpolating it into the Markdown table row returned by the heads map. Update
the head rendering in the map callback while preserving the existing shape,
scripts, occurrences, arity, and per-version columns.
- Around line 423-436: Update the --check flow in the explain operator census
command to return a nonzero status whenever result.resolution.warning is set,
even if diffAgainstFixture returns no drift. Keep the existing drift reporting
and success path unchanged for cases without a resolution warning.

In `@scripts/explain-operator-slice.ts`:
- Around line 481-495: Validate each parsed capture before passing it to
buildSweep, requiring the expected precedence shape and reporting the offending
path with an actionable message. In the --out fixture read near outPath, handle
a missing or unreadable file with a clear error stating that the fixture must
already exist and contain the corpus block, while preserving the existing write
behavior.
- Around line 397-407: The explanatory text in the output-building logic should
derive the prefix-only operator count from the existing prefixOnly collection
instead of hardcoding “two.” Update the sentence near the prefixOnly.map block
to interpolate prefixOnly.length while preserving the surrounding wording and
generated README formatting.
- Around line 352-375: Move the README render-chain documentation block so it
immediately precedes the renderReadmeBlock function, leaving the cell function
and its markdown-escaping documentation together. Ensure each doc block directly
documents its intended symbol and preserve both documentation contents
unchanged.

In `@scripts/probes/explain-operator-sweep.ts`:
- Around line 95-99: Update ParseResult.deviceSource and its assignment in
describeParse to avoid claiming it is the exact device command when batch mode
adds marker and concatenation statements; either record the actual command
emitted by parseMany or narrow the field documentation to describe only the
parse wrapper.

In `@src/explain/operators.ts`:
- Around line 272-322: Update the NOT_OPERATORS doc comment to state that there
are eighteen word-shaped entries, matching all word-shaped rows in the array
from not through outside; leave the operator definitions and surrounding
explanation unchanged.

In `@test/unit/explain-operator-probe.test.ts`:
- Around line 45-96: Add a `verdictOf` test covering an accepted carrier whose
`ilHead` matches the spelling and whose `ilArity` is null, asserting it is
classified as an operator rather than `not-an-operator` and does not incorrectly
populate arities.
- Around line 1-25: Add direct unit tests in explain-operator-probe.test.ts for
the slice helpers outerCounts, precedenceLevels, associativityOf, and whyNot.
Cover equal-count grouping, non-conforming counts, and variadic,
left-associative, and right-associative cases, using the existing test
conventions and importing the helpers from explain-operator-slice.ts.

In `@test/unit/explain-operators.test.ts`:
- Around line 306-313: Remove the `runtime("any-nothing").output` assertion and
its 7.21.5 claim from the test; retain only the fixture-backed corpus assertions
in the test named ``any` has been an operator since at least 7.20.8`.
🪄 Autofix

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: ae7b2eaf-f540-4597-9ce5-6dc7ba0a591d

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3df39 and 8e72d84.

📒 Files selected for processing (12)
  • .github/workflows/ci.yaml
  • GLOSSARY.txt
  • commands/explain/README.md
  • package.json
  • scripts/explain-operator-census.ts
  • scripts/explain-operator-slice.ts
  • scripts/probes/AGENTS.md
  • scripts/probes/explain-operator-sweep.ts
  • src/explain/operators.ts
  • test/fixtures/explain/operators.json
  • test/unit/explain-operator-probe.test.ts
  • test/unit/explain-operators.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: copilot-pull-request-reviewer
🧰 Additional context used
📓 Path-based instructions (8)
**/AGENTS.md

📄 CodeRabbit inference engine (AGENTS.md)

Use directory-level AGENTS.md files only for local constraints.

Files:

  • scripts/probes/AGENTS.md
{docs/**,.github/instructions/**,.github/**/*.yml,.github/**/*.yaml,**/*.{md,txt,dict}}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun run lint:ci when changing documentation, instructions, security configuration, spelling dictionaries, or workflow files.

Files:

  • scripts/probes/AGENTS.md
  • .github/workflows/ci.yaml
  • GLOSSARY.txt
  • commands/explain/README.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Transport or RouterOS-touching code is not done until bun run test:integration passes.
Do not silently fall back to another protocol when the caller pinned --via.
Do not make generated output the hand-edited source of truth.
Do not disable validation to make a test pass; validation is part of the product.

Files:

  • test/unit/explain-operator-probe.test.ts
  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • scripts/explain-operator-census.ts
  • scripts/probes/explain-operator-sweep.ts
test/fixtures/**/*

📄 CodeRabbit inference engine (test/AGENTS.md)

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

Files:

  • test/fixtures/explain/operators.json
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

Files:

  • src/explain/operators.ts
commands/*/README.md

📄 CodeRabbit inference engine (commands/AGENTS.md)

commands/*/README.md: Each commands/<name>/README.md is the executable specification's designed tier: document intent, flags, and behavior; include a Designed, not implemented table for spec-only flags; do not duplicate implemented-flag tables generated in docs/CLI.md; link to docs/CONSTITUTION.md rather than restating constitution-wide rules; document only command-specific behavior; and keep the Status line consistent with docs/MATRIX.md.
Implemented flags must be generated from CliCommandMetadata into docs/CLI.md via bun run docs:cli; command READMEs must not maintain duplicate tables of implemented flags.

Files:

  • commands/explain/README.md
commands/*/{README.md,examples.md}

📄 CodeRabbit inference engine (AGENTS.md)

Read the target command's README.md and examples.md as the executable specification before writing code or tests.

Files:

  • commands/explain/README.md
scripts/probes/**/*.ts

📄 CodeRabbit inference engine (scripts/probes/AGENTS.md)

scripts/probes/**/*.ts: Both oracles must see the same bytes.
Import @tikoci/quickchr through ./chr.ts.
Resolve the corpus through ../corpus-fetch.ts.
An abstention is not a disagreement.

Files:

  • scripts/probes/explain-operator-sweep.ts
🧠 Learnings (32)
📓 Common learnings
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 224
File: commands/explain/examples.md:11-16
Timestamp: 2026-08-06T14:17:50.227Z
Learning: For the phase-1 offline explain API, offline behavior is tested through unit and fixture tests rather than CHR-backed integration tests. In `test/unit/explain-envelope.test.ts`, the CLI-spelling gate-versus-analysis behavior described by `commands/explain/examples.md` example `1b` and the resolved-menu behavior described by example `18b` have focused unit coverage. The CLI `explain` command surface is deferred and is not implemented as `src/cli/explain.ts` in this PR.
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: src/protocols/AGENTS.md:0-0
Timestamp: 2026-06-08T22:26:06.293Z
Learning: Applies to src/protocols/**/*router*{protocol,implementation,api,cli}*.{js,ts,md} : Ground protocol facts before implementation: RouterOS service/API/CLI path, auth model, default port, local tooling, validation source, security warnings, failure modes, and CHR test shape
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-06-16T10:51:07.659Z
Learning: Prefer RouterOS CHR integration tests through `quickchr` over complex mocks when behavior depends on RouterOS.
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-06-08T22:25:53.303Z
Learning: Preserve RouterOS syntax and semantics. Do not add high-level RouterOS configuration helpers; that boundary is fixed in `docs/CONSTITUTION.md`
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 0
File: :0-0
Timestamp: 2026-06-26T01:58:28.177Z
Learning: For the btest CHR integration coverage in this repository, UDP client `receive` and `both` modes are now gated; the remaining unproven UDP edge is the server cell's host→guest direction, which would require UDP host forwarding and remains documented as an open caveat.
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 227
File: src/explain/args.ts:151-158
Timestamp: 2026-08-06T23:42:39.898Z
Learning: For the RouterOS explain argument lexer corpus checks, a green frozen corpus measures coverage and risk but does not validate lexical rules absent from the corpus. The frozen corpus contains no lone-`\r` continuation case, so edge-case probes and targeted regression tests remain required for continuation behavior.
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: commands/AGENTS.md:0-0
Timestamp: 2026-07-23T00:15:08.643Z
Learning: Route RouterOS `add`, `set`, and `remove` operations through `execute`, with protocol selection governed by the constitution.
📚 Learning: 2026-08-06T14:17:50.227Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 224
File: commands/explain/examples.md:11-16
Timestamp: 2026-08-06T14:17:50.227Z
Learning: For the phase-1 offline explain API, offline behavior is tested through unit and fixture tests rather than CHR-backed integration tests. In `test/unit/explain-envelope.test.ts`, the CLI-spelling gate-versus-analysis behavior described by `commands/explain/examples.md` example `1b` and the resolved-menu behavior described by example `18b` have focused unit coverage. The CLI `explain` command surface is deferred and is not implemented as `src/cli/explain.ts` in this PR.

Applied to files:

  • scripts/probes/AGENTS.md
  • package.json
  • test/unit/explain-operator-probe.test.ts
  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • commands/explain/README.md
  • scripts/explain-operator-census.ts
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-08-11T17:46:27.539Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 273
File: test/fixtures/explain/corpus-partition.json:4-4
Timestamp: 2026-08-11T17:46:27.539Z
Learning: For durable fixtures in this repository, do not cite in-flight `.scratch/` files as provenance. In `test/fixtures/explain/corpus-partition.json`, the committed groups are the durable artifact, and the `frozen` no-regeneration statement replaces the former `.scratch/explain-lab-partition.ts` generator citation.

Applied to files:

  • scripts/probes/AGENTS.md
  • .github/workflows/ci.yaml
  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-census.ts
📚 Learning: 2026-08-05T20:06:21.299Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 217
File: src/explain/symbols.ts:0-0
Timestamp: 2026-08-05T20:06:21.299Z
Learning: In `src/explain/symbols.ts`, comparative corpus measurements that explain a semantic modeling trade-off are rule provenance and follow the established F1/F2 documentation convention. Treat run-specific bookkeeping, fuzz counts, scratch-script paths, method pointers, and per-version missed-tail details as status data that should not remain in source comments.

Applied to files:

  • scripts/probes/AGENTS.md
  • package.json
  • test/unit/explain-operator-probe.test.ts
  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • commands/explain/README.md
  • scripts/explain-operator-census.ts
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-08-05T03:19:54.280Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 209
File: test/unit/explain-menus.test.ts:146-152
Timestamp: 2026-08-05T03:19:54.280Z
Learning: In `test/unit/explain-write.test.ts`, keep separate test anchors for `isDanglingBarePath` and menu-table rejection. Known menu paths such as `/ip/firewall/filter` and `/log` reach `isDanglingBarePath` at document end, while `/system/reboot` is rejected earlier by `isConfirmedNav` because `isMenuPath` returns false. Do not combine these inputs in one test when asserting coverage of either mechanism.

Applied to files:

  • scripts/probes/AGENTS.md
  • package.json
  • test/unit/explain-operator-probe.test.ts
  • test/unit/explain-operators.test.ts
📚 Learning: 2026-08-06T05:38:13.259Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 222
File: test/fixtures/explain/defects.json:3-73
Timestamp: 2026-08-06T05:38:13.259Z
Learning: In `src/explain/pathresolve.ts`, `Loc.base = -1` widening applies to defects raised during nested walker traversal, such as `over-depth`, when their local offsets cannot be mapped safely from UTF-16 string indexes to analyzed UTF-8 byte offsets. Defects emitted by the top-level segmenter are already in analyzed-document byte space and therefore retain precise offsets even when their enclosing statement contains non-ASCII text. The behavior is covered by `test/fixtures/explain/defects.json` and `test/unit/explain-defects.test.ts`.

Applied to files:

  • scripts/probes/AGENTS.md
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
📚 Learning: 2026-08-06T05:38:01.223Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 222
File: src/explain/defects.ts:1-70
Timestamp: 2026-08-06T05:38:01.223Z
Learning: In the TypeScript `src/explain` modules, references to phase-0 lab questions and issue numbers are an established documentation idiom when they explain the technical basis for a current contract. Keep these rationale references when they support implementation behavior. Remove dated decisions, delivery status, and future roadmap text from implementation and test comments.

Applied to files:

  • scripts/probes/AGENTS.md
  • test/unit/explain-operators.test.ts
  • commands/explain/README.md
📚 Learning: 2026-08-06T05:37:57.342Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 222
File: src/explain/pathresolve.ts:895-901
Timestamp: 2026-08-06T05:37:57.342Z
Learning: In `src/explain/pathresolve.ts`, over-depth defects must use `regionIn(loc, start, end)` rather than `spanIn` when the scanned extent can be empty. `regionIn` widens empty bracket or block-body extents to the enclosing statement span, preserving the `Defect` contract that `end > start` without fabricating a precise byte location.

Applied to files:

  • scripts/probes/AGENTS.md
  • src/explain/operators.ts
📚 Learning: 2026-08-01T23:46:54.392Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 197
File: src/explain/verbsplit.ts:383-387
Timestamp: 2026-08-01T23:46:54.392Z
Learning: In `src/explain/verbsplit.ts`, `VerbSplit` is the ratified Q6 boundary shape. Do not add document-scale context-certainty metadata to it independently; add that signal with the planned `ambiguous`/`unknown` verdict-vocabulary and phase-1 envelope work tracked in GitHub issue `#192`. Until then, `resolveVerbs` may correctly return root-based resolved paths for context-independent statements after context loss, but callers cannot observe that certainty was lost.

Applied to files:

  • scripts/probes/AGENTS.md
📚 Learning: 2026-08-06T23:42:39.897Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 227
File: src/explain/args.ts:151-158
Timestamp: 2026-08-06T23:42:39.897Z
Learning: In the RouterOS explain lexer, `src/explain/args.ts` and `src/explain/verbsplit.ts` must use the shared `continuationLength(text, at)` helper for backslash-newline handling. Only `\n` and `\r\n` are continuations. A lone `\r` after `\` must not be skipped; `args.ts` must refuse it as an invalid escape to preserve source-accurate token names and spans.

Applied to files:

  • scripts/probes/AGENTS.md
  • scripts/explain-operator-slice.ts
  • commands/explain/README.md
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-08-10T23:41:13.189Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 256
File: src/explain/args.ts:936-945
Timestamp: 2026-08-10T23:41:13.189Z
Learning: In `src/explain/args.ts`, `pushArrayMembers` must withdraw the enclosing array shape with `DEPTH_BOUND_REACHED` when `depth >= MAX_MEMBER_DEPTH`; returning `null` incorrectly reports the unverified literal as a valid array. CHR 7.23.3 accepts valid array nesting to at least depth 64, while invalid `(1,)` members remain syntax errors at all tested depths. The 948-script corpus has a maximum observed array-member depth of 6, so the depth-8 analysis bound does not drop observed valid corpus literals.

Applied to files:

  • scripts/probes/AGENTS.md
  • test/unit/explain-operator-probe.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • commands/explain/README.md
  • scripts/explain-operator-census.ts
📚 Learning: 2026-08-02T03:18:27.819Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 200
File: src/explain/symbols.ts:525-535
Timestamp: 2026-08-02T03:18:27.819Z
Learning: In `src/explain/symbols.ts`, RouterOS accepts both `:onerror NAME` and bare `onerror NAME` as error-variable directives. Both forms bind `NAME` as `local`; do not require the `:` sigil for `ERRVAR_HEADS`. This matches `src/explain/blocks.ts` `DIRECTIVE_BODY` and fixture `F3a` in `test/fixtures/explain/symbols.json`, which records CHR 7.23.2 `/console/inspect request=highlight` evidence.

Applied to files:

  • scripts/probes/AGENTS.md
  • test/fixtures/explain/operators.json
  • test/unit/explain-operators.test.ts
  • commands/explain/README.md
📚 Learning: 2026-06-16T10:51:07.659Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-06-16T10:51:07.659Z
Learning: Applies to test/**/cli-smoke.test.ts : Network-free CLI smoke tests (`cli-smoke.test.ts`) should not be CHR-gated and should run in the fast push/PR gate (`bun test`).

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-08-11T03:49:52.355Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 265
File: .github/workflows/ci.yaml:171-172
Timestamp: 2026-08-11T03:49:52.355Z
Learning: In this repository's GitHub Actions workflows, SHA-pin `oven-sh/setup-bun` and `actions/setup-node` references and include version comments. Other first-party `actions/*` references may continue using floating major-version tags according to the repository's current convention. Do not request SHA pinning for those first-party actions unless a repository-wide maintainer policy and corresponding Dependabot update policy have been established.

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-08-09T05:13:01.339Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 0
File: :0-0
Timestamp: 2026-08-09T05:13:01.339Z
Learning: RouterOS `.` is an expression concatenation operator with array-distributing semantics. In the current offline value-shape phase, the strict token reader refuses parenthesized, braced, and array forms before `valueShapeHints` runs. Future `observedType` and expression parsing must not treat `a.b` or `1.2` as a single atomic literal without expression-aware parsing.

Applied to files:

  • GLOSSARY.txt
  • test/unit/explain-operators.test.ts
  • commands/explain/README.md
📚 Learning: 2026-08-11T13:12:33.293Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 268
File: src/explain/args.ts:0-0
Timestamp: 2026-08-11T13:12:33.293Z
Learning: In `src/explain/args.ts`, RouterOS brace-array member keys use the measured grammar `^[A-Za-z0-9./-]+$`: `.`, `-`, and `/` are valid anywhere, including alone or repeated. Examples include `{.=1}`, `{..id=1}`, `{-=1}`, `{--=1}`, `{/=1}`, and `{a/b=1}`. `_` is not valid in this key grammar: `{_a=1}` and `{a_b=1}` are comparisons. Key binding still requires the key to touch `=`; `{.id =1}` is a `bool` comparison, while `{.id=1}` binds key `.id`. When a spelling is not recognized as a key, `pushArrayMembers()` must abstain unless `NOT_IN_MEMBER_NAME` positively proves an expression byte.

Applied to files:

  • GLOSSARY.txt
  • test/fixtures/explain/operators.json
  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • commands/explain/README.md
📚 Learning: 2026-08-11T12:52:27.150Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 268
File: src/explain/args.ts:896-896
Timestamp: 2026-08-11T12:52:27.150Z
Learning: In RouterOS array literals, dotted keys such as `.id` and `.id111` are valid keys. For example, `{.id=1}` stores the value under the key `".id"`; access must quote the dotted key (`$a->".id"`), while `$a->.id` does not access that key. The `memberKey()` parser in `src/explain/args.ts` must accept this dotted-key spelling.

Applied to files:

  • GLOSSARY.txt
  • test/unit/explain-operators.test.ts
  • src/explain/operators.ts
📚 Learning: 2026-08-07T16:47:13.907Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T16:47:13.907Z
Learning: Applies to {docs/**,.github/instructions/**,.github/**/*.yml,.github/**/*.yaml,**/*.{md,txt,dict}} : Run `bun run lint:ci` when changing documentation, instructions, security configuration, spelling dictionaries, or workflow files.

Applied to files:

  • package.json
📚 Learning: 2026-08-07T16:47:13.907Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T16:47:13.907Z
Learning: Run `bun run lint && bun run test && bun run build` before finishing code changes.

Applied to files:

  • package.json
📚 Learning: 2026-08-08T00:38:33.949Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 234
File: .claude/settings.json:0-0
Timestamp: 2026-08-08T00:38:33.949Z
Learning: In `.claude/settings.json`, the `Bash(biome *)`, `Bash(tsc *)`, `Bash(cspell *)`, `Bash(markdownlint-cli2 *)`, `Bash(secretlint *)`, and `Bash(shellcheck *)` permissions intentionally allow valid lint command variants. `Bash(bun *)` already permits the repository’s `bun run lint*` commands. Do not request narrowing only these direct lint-tool permissions unless the broader Bun permission model also changes.

Applied to files:

  • package.json
📚 Learning: 2026-08-06T21:57:06.372Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 226
File: test/integration/cli-smoke.test.ts:333-347
Timestamp: 2026-08-06T21:57:06.372Z
Learning: For the `centrs explain` CLI, ambient stdin is read only when no positional argument can provide the offline input. When a positional input is present and stdin is supplied, the explain result includes the `usage/stdin-ignored` warning. In `test/integration/cli-smoke.test.ts`, no-input coverage must test both child stdin shapes: no `stdin` option (`/dev/null`) and `stdin: ""` (an empty pipe).

Applied to files:

  • test/unit/explain-operator-probe.test.ts
📚 Learning: 2026-08-09T00:35:51.834Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 241
File: src/explain/transport.ts:0-0
Timestamp: 2026-08-09T00:35:51.834Z
Learning: In `src/explain/transport.ts`, offline `print where` transport classification must fail closed unless each query expression is an attribute token with a valid property name and a non-empty decoded value. The runtime-exercised Q8 shape is `name=value`; bare property names, empty values, infix comparisons such as `address>1.1.1.1`, REST comparison words such as `>name=value`, and repeated `where` tokens must not produce a REST `.query` request.

Applied to files:

  • test/unit/explain-operator-probe.test.ts
  • commands/explain/README.md
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-08-09T05:23:19.794Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 242
File: src/explain/values.ts:83-90
Timestamp: 2026-08-09T05:23:19.794Z
Learning: In `src/explain/values.ts`, RouterOS time literals are order-independent and additive on RouterOS 7.23.3 and 7.24rc3. The `isTimeShape` function must use full fragment coverage rather than enforce descending time-unit order or reject repeated units. Examples such as `1s1m`, `1m1m`, `1h1h1h`, and `1s1ms` are valid `time` values.

Applied to files:

  • test/unit/explain-operators.test.ts
  • scripts/explain-operator-slice.ts
  • src/explain/operators.ts
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-06-16T10:51:07.659Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: test/AGENTS.md:0-0
Timestamp: 2026-06-16T10:51:07.659Z
Learning: Applies to test/integration/**/*.test.{ts,js} : Put long-running, RouterOS-backed, or platform-specific tests (including process-level tests that spawn the real `src/cli.ts` through `cli-process.ts` and network-free CLI smoke tests in `cli-smoke.test.ts`) under `test/integration/` and wire them through QA or lab workflows.

Applied to files:

  • test/unit/explain-operators.test.ts
📚 Learning: 2026-08-07T16:47:13.907Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T16:47:13.907Z
Learning: Applies to **/*.{ts,tsx} : Transport or RouterOS-touching code is not done until `bun run test:integration` passes.

Applied to files:

  • test/unit/explain-operators.test.ts
📚 Learning: 2026-06-08T22:26:06.293Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: src/protocols/AGENTS.md:0-0
Timestamp: 2026-06-08T22:26:06.293Z
Learning: Applies to src/protocols/**/*router*{protocol,implementation,api,cli}*.{js,ts,md} : Ground protocol facts before implementation: RouterOS service/API/CLI path, auth model, default port, local tooling, validation source, security warnings, failure modes, and CHR test shape

Applied to files:

  • test/unit/explain-operators.test.ts
  • src/explain/operators.ts
  • scripts/probes/explain-operator-sweep.ts
📚 Learning: 2026-08-06T23:42:39.898Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 227
File: src/explain/args.ts:151-158
Timestamp: 2026-08-06T23:42:39.898Z
Learning: For the RouterOS explain argument lexer corpus checks, a green frozen corpus measures coverage and risk but does not validate lexical rules absent from the corpus. The frozen corpus contains no lone-`\r` continuation case, so edge-case probes and targeted regression tests remain required for continuation behavior.

Applied to files:

  • test/unit/explain-operators.test.ts
📚 Learning: 2026-08-06T05:37:57.693Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 222
File: src/explain/defects.ts:1-70
Timestamp: 2026-08-06T05:37:57.693Z
Learning: In TypeScript modules under src/explain, retain references to phase-0 lab questions and issue numbers when they document the technical rationale for current implementation behavior or contracts. Remove comments containing dated decisions, delivery status, or future roadmap information when it does not explain current behavior, including in tests.

Applied to files:

  • src/explain/operators.ts
📚 Learning: 2026-06-08T22:25:53.303Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-06-08T22:25:53.303Z
Learning: Preserve RouterOS syntax and semantics. Do not add high-level RouterOS configuration helpers; that boundary is fixed in `docs/CONSTITUTION.md`

Applied to files:

  • commands/explain/README.md
📚 Learning: 2026-08-06T22:07:13.134Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 226
File: commands/explain/README.md:0-0
Timestamp: 2026-08-06T22:07:13.134Z
Learning: For `centrs explain` documentation, an instructed remedy must be executable in the current phase. The live `centrs explain <router> --file -` form is parsed but returns `usage/not-implemented` until phase 2, so documentation must present the offline `--file -` form as the actionable stdin remedy.

Applied to files:

  • commands/explain/README.md
📚 Learning: 2026-08-09T00:35:54.042Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 241
File: src/explain/transport.ts:0-0
Timestamp: 2026-08-09T00:35:54.042Z
Learning: In `tikoci/centrs` `src/explain/transport.ts`, the offline explain transport contract treats the Q8-probed `run(action) → POST /rest/<path>/<command>` result as a general menu-action URL rule. Verbs outside literal `add`, `get`, `set`, `remove`, `print`, and `find` classify as `api-candidate` action POSTs when their arguments contain only attributes. The action endpoint preserves the verb's original case. `find`, positional action operands, and action queries remain `unknown`.

Applied to files:

  • commands/explain/README.md
📚 Learning: 2026-08-07T16:47:13.907Z
Learnt from: CR
Repo: tikoci/centrs PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-07T16:47:13.907Z
Learning: A feature is done only when its examples pass on real CHR via `bun run test:integration`; existing code and passing unit tests alone are insufficient.

Applied to files:

  • scripts/probes/explain-operator-sweep.ts
🪛 ast-grep (0.45.1)
scripts/probes/explain-operator-sweep.ts

[warning] 798-805: Avoid logging sensitive data
Context: console.log(
${TAG[row.verdict]} ${row.token.padEnd(6)} +
arities=${row.operatorArities.join(",") || "-"} .padEnd(14) +
heads=${row.ilHeads.join(",") || "-"} .padEnd(14) +
hl=${(row.highlight.run?.class ?? "-").padEnd(20)} +
run=${JSON.stringify(row.highlight.run?.text ?? "")} +
${row.highlight.runIsTokenExactly ? "" : " (run != token)"},
)
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)


[warning] 875-877: Avoid logging sensitive data
Context: console.log(
${token.padEnd(6)} outer ${String(outerCount.get(token) ?? 0).padStart(3)}/${seen},
)
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)


[warning] 946-951: Avoid logging sensitive data
Context: console.log(
${verdict.padEnd(16)} ${rows .filter((r) => r.verdict === verdict) .map((r) => r.token) .join(" ")},
)
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🪛 OpenGrep (1.26.0)
scripts/explain-operator-census.ts

[ERROR] 405-410: SQL query built via string concatenation or template literal passed to query()/execute(). Use parameterized queries instead.

(coderabbit.sql-injection.raw-query-concat-js)

scripts/probes/explain-operator-sweep.ts

[ERROR] 68-68: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 203-203: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (31)
GLOSSARY.txt (1)

696-700: LGTM!

commands/explain/README.md (1)

1043-1101: LGTM!

Also applies to: 1103-1140, 1142-1187, 1189-1193

package.json (1)

67-67: LGTM!

Also applies to: 100-104, 108-108

.github/workflows/ci.yaml (1)

160-164: LGTM!

Also applies to: 215-228

scripts/probes/AGENTS.md (1)

23-23: LGTM!

scripts/probes/explain-operator-sweep.ts (10)

67-93: LGTM!


215-277: LGTM!


300-316: LGTM!


327-454: LGTM!


463-661: LGTM!


705-748: LGTM!


750-806: LGTM!


808-883: LGTM!


885-962: LGTM!


284-290: 🎯 Functional Correctness

Confirm highlight CSV behavior on RouterOS 7.23.3 and 7.24rc4. Existing captures for 7.23.2 and 7.24rc2 contain no empty class fields. The positional bug is conditional: an interior empty field would misalign coalesce, but current evidence does not show that either target version emits one. Add captures or a regression fixture before changing this parser.

scripts/explain-operator-census.ts (6)

72-81: LGTM!


142-196: LGTM!


208-287: LGTM!


319-346: LGTM!


355-385: LGTM!


454-461: LGTM!

scripts/explain-operator-slice.ts (3)

225-225: outerCount stores null for a never-outer spelling such as ->, which collides with the prefix-only !/any "not measured" meaning. Store 0 instead.


255-277: versionDifferences diffs only runtime and verdict. Precedence, associativity, and opAxis differences do not surface, so the "identical except one runtime row" claim is not enforced.


162-188: LGTM!

test/fixtures/explain/operators.json (2)

1455-1472: example is the first carrier's IL, not the carrier that justifies whyNot. The evl row and the $ row both show syntax error ... next to a residual-variable or juxtaposition verdict.


1020-1307: LGTM!

src/explain/operators.ts (2)

121-173: LGTM!


403-411: LGTM!

test/unit/explain-operator-probe.test.ts (1)

140-169: LGTM!

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

78-138: LGTM!


251-258: LGTM!

Comment thread scripts/explain-operator-census.ts
Comment thread scripts/explain-operator-census.ts
Comment thread scripts/explain-operator-slice.ts Outdated
Comment thread scripts/explain-operator-slice.ts Outdated
Comment thread scripts/explain-operator-slice.ts Outdated
Comment thread scripts/probes/explain-operator-sweep.ts
Comment thread src/explain/operators.ts
Comment thread test/unit/explain-operator-probe.test.ts
Comment thread test/unit/explain-operator-probe.test.ts
Comment thread test/unit/explain-operators.test.ts
Works the PR #287 review from muse, Copilot and CodeRabbit. Two review
items were the same defect — a claim published without evidence a reader
could reach — so both are fixed by measuring, not by softening the words.

Device work (three CHRs, one per release channel):
- adds 7.21.5 long-term as a third capture. A test comment claimed
  "7.21.5 long-term still answers `any`" while asserting a row from the
  primary capture; the fixture held no 7.21.5 evidence at all.
- adds the unary-vs-binary sweep: every `(U 1 B 2)` and `(1 B U 2)` for
  U in !/any/~/-/>, 240 probes. The README asserted this lattice from a
  local run no capture held. Measured: 240/240 accepted, binary outer in
  every one, identical on all three versions.
- completes the dot-spacing matrix. New finding: `(1. 2)` is NOT concat
  — `1.` lexes as a variable name and the row is juxtaposition,
  `(  $1. 2)`. `(1 .2)` and `(1 . 2)` are both `(. 1 2)`.
- re-cuts all three captures from the committed probe, so the fixture is
  reproducible from source. It was not: the probe said `bogus-symbol`
  and the fixture said `bogus-punct`.

Slice:
- diffs every published axis per version, not just runtime and verdict.
  Mutation-tested: 4 planted mutations produce 11 rows across all 6
  kinds. "Identical except one runtime row" is now measured.
- `outerCount` separates measured-never-outer (`->` = 0) from
  never-measured (prefix-only = null); they were both null.
- a non-operator's `example` is now the carrier that EARNS its verdict.
  `evl` showed a syntax error next to a residual-variable verdict.
- validates capture shape and names the fixture prerequisite instead of
  failing with an iteration TypeError or a bare ENOENT.
- derives the prefix-only count in the generated prose (#260).

Census:
- escapes `|` in head cells. IL carries unquoted string content into
  head position (`num|str`), which split those rows in the job summary.
- `--check` refuses a corpus that is not the pinned snapshot rather than
  warning and returning 0.

Docs/tests: word-shaped count 11 -> 18; `deviceSource` no longer claims
to be the whole console command; `[` named in UNDOCUMENTED; unit
coverage for the slice derivations and a verdictOf null-arity case.

Not changed: `explainCommand` output is byte-identical to main on six
baselines. Still grounding-only.

Rejected: Copilot's two `\|` reports. GFM consumes the escape inside a
code span — verified against GitHub's renderer, which emits <code>|
</code>. Removing it would break the table rows instead.

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

Copy link
Copy Markdown
Contributor Author

Review worked — pushed as 0db913b

All 19 inline items addressed: 17 fixed, 2 rejected with evidence. Replies are on each thread.

Two review items were the same defect, and it is the one worth naming: a claim published without evidence a reader could reach. CodeRabbit caught it on the 7.21.5 test line; the README had a bigger one. Both are fixed by measuring, not by softening the words.

Went back to the device (three CHRs, one per release channel)

  • 7.21.5 long-term added as a third capture. The test comment said "7.21.5 long-term still answers any" while asserting a row that came from the primary capture. Now swept for real.
  • Unary precedence is measured, not asserted. The README claimed a lattice over (U 1 B 2)/(1 B U 2) for U!,any,~,-,> from a local run no capture held. It is now a sub-sweep: 240 probes per version, 240 accepted, binary outer in every one, identical on all three builds. Published as sweep.unary, whose load-bearing field is exceptions — empty is the finding.
  • Dot-spacing matrix completed (muse's item 3), and it produced a new finding: (1. 2) is not concat. 1. lexes as a variable name and the row comes back as juxtaposition, ( $1. 2). (1 .2) and (1 . 2) are both (. 1 2). So a fill rule that claims every . byte emits a span the device does not have — directly relevant to explain: the token border — a total offline token stream, with LSP/SCIP vocabulary as the last step #264 B2.
  • All three captures re-cut from the committed probe. The bogus-symbol/bogus-punct mismatch Copilot found was not churn: it meant the fixture could not be regenerated from source. It can now.

The version claim is now measured

buildSweep diffs every axis the fixture publishes — verdict, arities, highlight run, precedence, associativity, unary placement, op-axis, conformance — not just runtime and verdict.

Mutation-tested rather than asserted: 4 planted mutations in one capture produce 11 diff rows across all 6 kinds. Three of those four were previously invisible.

Result: still exactly one differing fact across 7.21.5 / 7.23.3 / 7.24rc4, the array-compare runtime row. It now shows the two errors are not the same error — 7.23.3 says cannot compare if array is more than array, 7.21.5 says cannot compare if nothing is more than nothing, so on long-term the {2;1} operands did not survive to the comparison.

Rejected, with evidence

Copilot's two \| reports (explain-operator-slice.ts:375, README.md:1125). GFM consumes the escape inside a code span:

$ gh api markdown -f mode=gfm -f text='| s |\n| --- |\n| `\|` |'
<td><code class="notranslate">|</code></td>

That is a rendered |. Removing the escape would put a bare pipe in the cell and split the row — the bug it was added for. CodeRabbit flagged the same construct in explain-operator-census.ts in the opposite direction (not escaping, rendering into the CI job summary); that one was real and is fixed. Verification noted in cell()'s doc comment.

Verification

lint:ci green · 2716 unit tests, 0 fail · build clean · census + README gates green · explainCommand byte-identical to main on six baselines (checked against a main worktree). Still grounding-only — no spans[] change, no new span class. Emission stays #264 B2/B4.

op-type residue filed separately as #288, per the maintainer's call: it is a values decision, not a lexer one.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review the latest diff, and resolve old threads if fixed

@tikoci tikoci deleted a comment from coderabbitai Bot Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="tikoci/centrs"
pr_number="287"

printf '%s\n' '== PR head and changed files =='
gh pr view "$pr_number" --repo "$repo" --json headRefOid,baseRefOid,updatedAt,files \
  --jq '{headRefOid, baseRefOid, updatedAt, files: [.files[] | {path, additions, deletions}]}'

printf '%s\n' '== Authenticated account =='
gh api user --jq '{login, id}'

printf '%s\n' '== Unresolved review threads =='
gh api graphql \
  -F owner="${repo%%/*}" \
  -F name="${repo#*/}" \
  -F number="$pr_number" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          path
          line
          originalLine
          comments(first: 30) {
            nodes {
              id
              author { login }
              body
              createdAt
              url
            }
          }
        }
      }
    }
  }
}' \
  --jq '.data.repository.pullRequest.reviewThreads.nodes
    | map(select(.isResolved == false))
    | map({
        id, path, line, originalLine,
        comments: [.comments.nodes[] | {author: (.author.login // "ghost"), body, url}]
      })'

Length of output: 2845


🏁 Script executed:

#!/bin/bash
set -u

repo="tikoci/centrs"
pr_number="287"

printf '%s\n' '== Diff checks =='
git diff --check 2f3df397ea21285f7d576f27f55cfb8591cb0996 0db913b3e2da2bfe6ed79e04601a34a8bddded43 || true
git diff --stat 2f3df397ea21285f7d576f27f55cfb8591cb0996 0db913b3e2da2bfe6ed79e04601a34a8bddded43

printf '%s\n' '== Changed TypeScript outlines =='
ast-grep outline scripts/explain-operator-census.ts --items all --view expanded 2>&1 || true
ast-grep outline scripts/explain-operator-slice.ts --items all --view expanded 2>&1 || true
ast-grep outline scripts/probes/explain-operator-sweep.ts --items all --view expanded 2>&1 || true
ast-grep outline src/explain/operators.ts --items all --view expanded 2>&1 || true

printf '%s\n' '== Unresolved review threads =='
gh api graphql \
  -F owner="${repo%%/*}" \
  -F name="${repo#*/}" \
  -F number="$pr_number" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          path
          line
          originalLine
          comments(first: 30) {
            nodes {
              author { login }
              body
              url
            }
          }
        }
      }
    }
  }
}' \
  --jq '.data.repository.pullRequest.reviewThreads.nodes
    | map(select(.isResolved == false))
    | map({
        id, path, line, originalLine,
        comments: [.comments.nodes[] | {author: (.author.login // "ghost"), body, url}]
      })' 2>&1 || true

Length of output: 19671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Prior finding implementations =='
sed -n '120,180p' src/explain/operators.ts
sed -n '200,390p' scripts/explain-operator-slice.ts
sed -n '395,445p' scripts/probes/explain-operator-sweep.ts
sed -n '515,630p' scripts/explain-operator-slice.ts

printf '%s\n' '== Generated Markdown pipe rows =='
rg -n -C2 --fixed-strings '\|' commands/explain/README.md

printf '%s\n' '== Tests that pin prior findings =='
rg -n -C4 -e "never outer" -e "never measured" -e "version difference" \
  -e "highlight run wider" -e "EARNS the verdict" \
  test/unit/explain-operators.test.ts test/unit/explain-operator-probe.test.ts

printf '%s\n' '== Diff: workflow and package gates =='
git diff --unified=80 2f3df397ea21285f7d576f27f55cfb8591cb0996 0db913b3e2da2bfe6ed79e04601a34a8bddded43 \
  -- .github/workflows/ci.yaml package.json

printf '%s\n' '== File-system and process side effects in new scripts =='
rg -n -C3 -e '\b(writeFileSync|unlinkSync|rmSync|execSync|spawn|Bun\.spawn|Database)\b' \
  scripts/explain-operator-census.ts scripts/explain-operator-slice.ts scripts/probes/explain-operator-sweep.ts

Length of output: 40222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Complete version-difference implementation =='
sed -n '350,510p' scripts/explain-operator-slice.ts

printf '%s\n' '== Version-difference and fixture tests =='
sed -n '360,465p' test/unit/explain-operators.test.ts
sed -n '250,370p' test/unit/explain-operator-probe.test.ts

printf '%s\n' '== Operator registry serialization =='
sed -n '360,470p' src/explain/operators.ts

printf '%s\n' '== Current sweep difference data =='
python3 - <<'PY'
import json
path = "test/fixtures/explain/operators.json"
with open(path, encoding="utf-8") as f:
    fixture = json.load(f)
sweep = fixture.get("sweep", {})
print(json.dumps({
    "versions": sweep.get("_source", {}).get("versions"),
    "versionDifferences": sweep.get("versionDifferences"),
    "unary": sweep.get("unary"),
}, indent=2))
PY

Length of output: 19553


@mobileskyfi Review finding:

  • Medium — controls is published but not version-diffed. scripts/explain-operator-slice.ts Line 480 returns controls in sweep, but the loop at Line 366 compares no primary.controls row with the other captures. A version-specific control acceptance or IL change will remain absent from sweep.versionDifferences. Add an ID-based control comparison that includes source, il, and accepted. Add a mutation test for that difference.

The earlier fixes are present and test-covered. This includes the outerCount 0 versus null distinction, whyNotExample(), [ candidate coverage, and the Markdown table pipe escaping.

I found no unresolved CodeRabbit-authored thread. The remaining threads belong to mobileskyfi or copilot-pull-request-reviewer, so I cannot resolve them through this integration.


🧠 Learnings used
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 287
File: test/unit/explain-operators.test.ts:320-360
Timestamp: 2026-08-12T19:55:32.402Z
Learning: For the RouterOS operator sweep, `test/fixtures/explain/operators.json` stores captures from RouterOS 7.21.5 long-term, 7.23.3 stable, and 7.24rc4 testing. `scripts/explain-operator-slice.ts` compares every published sweep axis, including unary precedence, across these captures and emits `sweep.versionDifferences`. The `any` operator has no version differences in these captures.

Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 287
File: scripts/explain-operator-census.ts:429-454
Timestamp: 2026-08-12T19:54:50.443Z
Learning: In the centrs census commands, a successful fixture-drift check is valid only when the resolved corpus is the pinned corpus. `scripts/explain-operator-census.ts` rejects `--check` when `resolution.warning` is set. `scripts/explain-value-census.ts` has the same warning-only resolution shape but was deliberately left unchanged in PR `#287` to keep the RouterOS operator-surface work scoped to issue `#255`.

You are interacting with an AI system.

CodeRabbit's follow-up on #287: `controls` was published in the `sweep`
block and never compared across versions, so a version-specific control
acceptance or IL change stayed absent from `versionDifferences`.

That is the worst axis to miss. The controls calibrate the whole sweep —
`(1 zzz 2)` is the known-failing control — so a build where one moved
invalidates every other row taken from it. Reporting that as "identical"
is exactly the failure the diff exists to prevent.

- adds an id-keyed control diff carrying `source`, `il` and `accepted`,
  including a control missing from a version.
- restates the rule mechanically in the comment: anything the `sweep`
  block publishes gets diffed here.
- adds three `buildSweep` tests. Mutation-checked both ways: deleting
  the new block turns the two positive cases red, restoring it turns
  them green.

No fixture change — all three captures still agree on every control.

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

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 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/explain/operators.ts:280

  • The field comment claims highlight is variable-undefined for every word in nonOperators(), but this file explicitly records evl with deviceHighlightClass: "syntax-meta" (and other spellings here are punctuation). This makes the comment misleading for readers and future maintainers.
export interface NonOperator {
	spelling: string;
	reason: NonOperatorReason;
	/** What `highlight` called it. `variable-undefined` for every word here. */
	deviceHighlightClass: string;
}

scripts/probes/explain-operator-sweep.ts:133

  • This docstring says “None of the candidate carriers contain a $”, but the sweep does include candidates/sources that legitimately contain $ (e.g. the $ candidate and op-axis rows like (>$x)). The residualVariable field is actually computed relative to each row’s source, so the comment should reflect that to avoid readers assuming $ can never appear in inputs.
	/**
	 * The device invented a `$` the source did not have.
	 *
	 * None of the candidate carriers contain a `$`, so any `$` in the IL means
	 * part of the spelling was lexed as a VARIABLE REFERENCE rather than as an

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.

explain/lexer: the operator axis is unemitted — RouterOS classes operators syntax-meta, centrs emits no span (and not/.. are not operators)

2 participants