Skip to content

explain: read an array literal member by member, and only where it is one (#225) - #256

Merged
mobileskyfi merged 5 commits into
mainfrom
agent/explain-value-interior
Aug 11, 2026
Merged

explain: read an array literal member by member, and only where it is one (#225)#256
mobileskyfi merged 5 commits into
mainfrom
agent/explain-value-interior

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Progresses #225 — the V1 interior slice. This does not close the issue: live
observedType and schemaType producers remain sequenced with #236.

What changed

Each member of a {…}/(…) literal is now its own values.occurrences[] row:
kind: "element", its own document-byte span and shapeHints, a parent
naming the container's result-local id, and name when the brace form spells a
key. This is the expression/parser foundation #239 S2 and #255 are sequenced
behind — every gap closes by ADDING a field, none by changing one.

The grounding round overturned the premise twice

Both were hints already shipping since #242 that the device contradicts.
Neither was corpus-reachable enough to have shown up as a failure.

1. A {…} array literal is a syntax error in a CLI command argument.
/console/inspect classes the { byte error and :parse refuses the
statement for every row below, while the same bytes one position over parse:

input { byte reading
/ip/route/add comment={1;2} error rejected
/ip/dns/set servers={1.1.1.1;8.8.8.8} error rejected — and servers is list-typed, which rules out a schema-shaped explanation
/interface/print .proplist={name;comment} error rejected
ip route add comment={1;2} error rejected
:log info message={1;2} error rejected
:local z {1;2} / :put {1;2} / :foreach i in={1;2} accepted

centrs returned pass + array on all of the rejected ones, including
source={ :return 1 } script bodies. A brace is now anchored only in a root
scripting directive's value slot (positional, or in=). Note the accepted
braces are per-attribute, not per-name: :execute script={…} parses while
/system/logging/action/add script={…} is a syntax error.

2. There is no class-based wide IPv4 field. values.ts allowed
1.<=16777215 / 1.1.<=65535; every part is one octet:

literal :typeof value
1.255 ip 1.0.0.255
1.256 time 00:00:01.256
1.16777215 time 00:00:01.167772150, never 1.255.255.255
1.1.256 str plain text

26 corpus literals — GPS coordinates like 54.3520, fractional-second log
timings like 00.000414 — were being hinted ip.

A member is an expression, so it has its own lexicon

Inside a literal RouterOS parses an expression, which changes the answer for
whole classes of spelling:

  • a bare word is a variable reference ({abc} lowers to $abc), and so is
    a full MAC ({00:11:22:33:44:55}) and any time literal past the signed 64-bit
    nanosecond range — the cliff is exactly 15250w/15251w, 106751d/106752d,
    9223372036s/9223372037s, in the colon spelling too;
  • *1 and +1 are hard syntax errors there, though :local x *1 is id;
  • hexadecimal is added (0x10 is 16, 0X10 is a variable);
  • ; splits braces and , splits parens — {1,2} is one member, a nested
    array — and (1;2) does not parse;
  • = binds a key in braces ({a=1}) and COMPARES in parens ((a=1,b=2) is two
    bool members);
  • an empty member is a syntax error ({;}, {;1}, {1;;2}, (1,)) and
    withdraws the enclosing array shape with it; {1;} is legal.

The bare comma spelling is the first genuinely plural hint

=1,2,3 is a syntax error nowhere, and only the argument's TYPE decides whether
the device splits it:

input :parse IL
servers=1.1.1.1,8.8.8.8 servers=1.1.1.1;8.8.8.8 — a list
dst-port=80,443 dst-port=;80;443 — a list
comment=a,b comment=a,b — one string
interface=ether1,ether2 interface=ether1,ether2 — one string

So a named attribute carries ["array", "str"] — both readings, neither
validated, which is what shapeHints.values is a list for. A directive's value
slot has no text reading (:local x 1,2 is a two-member array), so the hint is
singular there. No members are located for a bare comma run, because whether it
splits at all is a schemaType fact; the (1,2) spelling, where the delimiters
prove it, is descended into as usual.

Census is re-derivable now

#242's corpus figures were published from a throwaway script, so when this slice
changed the emission there was no way to recompute them under the same
definitions — only to guess which definition produced each number. Added
bun run explain:value-census (scripts/explain-value-census.ts), and the
fixture records the command beside the figures.

Evidence

  • 61 device-typed member rows in test/fixtures/explain/values.json
    (interiorGrounding), generated from the probe JSON rather than retyped, and
    scored one-sided: an abstention is always allowed, a contradicting type name
    never. That test is what caught the IPv4 bug.
  • Corpus, 948 scripts: 19,709 emitted values, of which 5,636 are members
    (529 keyed, 1,147 nested in another member). The four invariants are all 0 —
    boundary contradictions, invalid spans, dangling parents, containment breaks.
  • bun run lint:ci
  • bun test — 2,435 pass, 0 fail
  • bun run build
  • bun run test:integration146 pass, 0 fail on CHR 7.23.3, including new
    examples 28 and 28b

Not done here

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added detailed value analysis for parenthesized and brace-style arrays, including nested and keyed members.
    • Value results now identify individual array elements, inferred types, source spans, and containing arrays.
    • Added stricter handling for malformed, ambiguous, unsupported, and out-of-range literals.
    • Added a command to generate value-analysis census reports in Markdown or JSON.
  • Documentation

    • Expanded guidance and examples for array parsing, value contexts, separators, and shape hints.
  • Tests

    • Added broad coverage for arrays, literals, nesting, invalid syntax, and census metrics.

… one (#225)

#225 V1 (interior). Each member of a `{…}`/`(…)` literal becomes its own
`values.occurrences[]` row — `kind: "element"`, its own span and shape hints, a
`parent` naming the container, and `name` for a keyed member. This is the
expression/parser foundation #239 S2 and #255 are sequenced behind.

Grounding it on CHR 7.23.3 found two hints already shipping that the device
contradicts, and both are fixed here:

- A `{…}` array literal is a syntax error in a CLI command argument.
  `/console/inspect` classes the `{` byte `error` and `:parse` refuses
  `comment={1;2}`, `servers={1.1.1.1;8.8.8.8}` (a LIST-typed attribute, which
  rules out a schema-shaped reading), `.proplist={…}`, the relative spelling,
  and `:log info message={1;2}`; only a root scripting directive's value slot
  takes one. centrs returned `pass` + `array` on all of them, including
  `source={ :return 1 }` script bodies.
- There is no class-based wide IPv4 field. Every part is one octet: `1.256` is
  `time` `00:00:01.256` and `1.16777215` is `time`, never `1.255.255.255`. 26
  corpus literals — GPS coordinates, fractional-second timings — were `ip`.

A member is parsed as an EXPRESSION, so it has its own lexicon: a bare word is
a variable reference (`{abc}` is `$abc`), as is a MAC and any time literal past
2^63 nanoseconds (the cliff is `15250w`/`15251w`); `*1` and `+1` do not parse
there though `:local x *1` is `id`; hexadecimal is a number (`0x10` is 16,
`0X10` is a variable). `;` splits braces and `,` splits parens — `{1,2}` is ONE
member — and `=` binds a key in braces but compares in parens. An empty member
is a syntax error and withdraws the enclosing `array` shape with it.

The bare comma spelling is the first genuinely plural hint. `=1,2,3` is a syntax
error nowhere, and only the argument's type decides whether it splits
(`servers=1.1.1.1,8.8.8.8` lowers to a `;` list; `comment=a,b` stays one
string), so a named attribute carries `["array", "str"]` and a directive's value
slot carries `["array"]` alone. No members are located for it — the split is a
`schemaType` fact — while the `(1,2)` spelling is descended into as usual.

Also adds `bun run explain:value-census`. #242's corpus figures came from a
throwaway script, so when the emission changed there was no way to recompute
them under the same definitions; the fixture now records the command that
derives them.

Evidence
- 61 device-typed member rows in `test/fixtures/explain/values.json`, scored
  one-sided: an abstention is always allowed, a contradicting type name never.
  That test is what caught the IPv4 bug.
- Corpus (948 scripts): 19,709 values, 5,636 members (529 keyed, 1,147 nested);
  0 boundary contradictions, 0 invalid spans, 0 dangling parents, 0 containment
  breaks.
- `bun run lint:ci`, `bun test` (2,435 pass), `bun run build`
- `bun run test:integration` — 146 pass, 0 fail on CHR 7.23.3, including new
  examples 28 and 28b

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 18:38
@coderabbitai

coderabbitai Bot commented Aug 10, 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: fefd7445-dba8-4e9a-ae21-5c4dd43748ea

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 explain pipeline now supports array-element occurrences, nested parent links, context-specific shape inference, comma handling, and stricter literal validation. A SQLite-backed census CLI and expanded fixtures, tests, examples, and documentation validate the behavior.

Changes

Array-aware value analysis

Layer / File(s) Summary
Contextual value shape inference
src/explain/values.ts, test/unit/explain-values.test.ts
Value inference distinguishes argument and array-member contexts. It validates numeric, IP, boolean, string, and time literals and handles comma-separated values.
Array member lexing and anchoring
src/explain/args.ts, test/unit/explain-args.test.ts, test/unit/explain-values.test.ts
The lexer parses permitted brace and parenthesized arrays, keys, nested members, separators, malformed members, and parent links.
Explain occurrence relationships
src/explain.ts, src/index.ts, test/unit/explain.test.ts, test/integration/explain-values.test.ts, test/integration/chr.ts
Explain output records array and element occurrences with IDs, spans, shape hints, and optional parent IDs.
Corpus census and validation
scripts/explain-value-census.ts, test/fixtures/explain/values.json, commands/explain/README.md, commands/explain/examples.md, package.json, docs/MATRIX.md
The census command measures anchors, elements, nesting, spans, recovery, and shape distributions. Fixtures and examples document the updated results.

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

Sequence Diagram(s)

sequenceDiagram
  participant Script
  participant Explain
  participant lexValueAnchors
  participant valueShapeHints
  Script->>Explain: analyze RouterOS statement
  Explain->>lexValueAnchors: collect value anchors
  lexValueAnchors->>lexValueAnchors: parse array members and parent links
  lexValueAnchors->>valueShapeHints: infer contextual member shapes
  valueShapeHints-->>lexValueAnchors: return shape hints
  lexValueAnchors-->>Explain: return occurrences
  Explain-->>Script: return explained values
Loading

Possibly related issues

Possibly related PRs

  • tikoci/centrs#244: Directly related array parsing and value-shape changes in the same explain modules.
  • tikoci/centrs#242: Earlier value-anchor and shape-hint implementation extended here with member contexts and parent relationships.
  • tikoci/centrs#227: Earlier argument lexer changes extended here with array-member anchors.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and relevant but omits the required Links, Change type, and Notes sections from the repository template. Add the template sections and provide the issue links, change-type selection, validation run, and RouterOS/protocol assumptions.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: member-by-member array literal parsing with contextual gating.
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 agent/explain-value-interior

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

This PR advances explain’s offline value analysis (#225 V1) by flattening array literals into per-member values.occurrences[] entries (with parent linkage), and by introducing a distinct “array-member” shape-hint lexicon that matches RouterOS expression parsing inside {…}/(…).

Changes:

  • Descend located array literals and emit each member as kind: "element" with its own span, hints, optional key name, and parent id.
  • Split value-shape hinting by context (argument vs array-member), including corrected IPv4 shortcut bounds and member-only hex/time rules.
  • Add a re-derivable corpus census script (bun run explain:value-census) and refresh fixtures/docs/tests accordingly.

Reviewed changes

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

Show a summary per file
File Description
test/unit/explain.test.ts Adds unit coverage for new example 28/28b value-occurrence shapes and member spans.
test/unit/explain-values.test.ts Extends grounding + anchor tests for member lexicon, brace legality, comma spelling, and updated corpus invariants.
test/unit/explain-args.test.ts Updates anchor-lexer tests to enable brace-array scanning where appropriate.
test/integration/explain-values.test.ts Adds CHR-backed grounding for example 28/28b and validates device behavior for members/brace gating.
test/integration/chr.ts Allows integration evidence to reference letter-suffixed examples (e.g., 28b).
test/fixtures/explain/values.json Updates corpus + interior grounding fixture with member facts, invariants, and new census fields.
src/index.ts Exposes new public types (ExplainValueAnchorKind, ExplainValueContext).
src/explain/values.ts Introduces ValueContext and member-specific hint rules (hex, time range, comma behavior, fixed IPv4 shortcut bounds).
src/explain/args.ts Adds element anchors + member descent and gates brace-array parsing by statement/path context.
src/explain.ts Composes element occurrences into the envelope, including parent id linkage and member-context shape hinting.
scripts/explain-value-census.ts New script to compute the fixture’s corpus census deterministically.
package.json Adds explain:value-census script entry.
docs/MATRIX.md Updates phase-0.5 file list to include values.ts.
commands/explain/README.md Documents the new element-occurrence shape and interior rules (needs number sync per comments).
commands/explain/examples.md Adds examples 28 and 28b narrative and expected output shape.

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

Comment thread commands/explain/README.md Outdated
Comment thread commands/explain/examples.md Outdated
Comment thread src/explain/args.ts Outdated
Comment thread commands/explain/README.md Outdated
Comment thread commands/explain/README.md 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: 5

🤖 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 `@commands/explain/README.md`:
- Around line 743-752: Update the corpus census paragraph in README.md to match
the checked-in assertions in test/unit/explain-values.test.ts: use 19,709 value
occurrences, 13,168 strict comparable anchors, 6,541 recovered prefix anchors,
570 unread statements with anchors, and 814 arrays. Preserve the remaining
census details unless regenerated output requires corresponding corrections,
then run bun run lint:ci.

In `@scripts/explain-value-census.ts`:
- Around line 64-72: Update the Emitted interface to include an optional name
field, then remove the casts around the Emitted object literal and keyedElements
entry so TypeScript validates them directly. Preserve the existing keyedElements
counting behavior while relying on the expanded Emitted type.
- Around line 234-249: Update the main execution flow around main and its
promise handlers to avoid calling process.exit immediately after console output.
Assign the resolved status to process.exitCode, and set process.exitCode to 1 in
the catch handler while preserving the existing error logging, allowing stdout
to drain before termination.

In `@test/fixtures/explain/values.json`:
- Around line 607-611: Remove the duplicate probe rows from the fixture arrays:
retain only one `interiorGrounding.members` entry for literal `100000w` with
type `nothing` and value `""`, and only one `interiorGrounding.positions` entry
for the `/ip/route/add comment=[:tostr {1;2}]` input with `parses: true`.
Preserve all other fixture entries unchanged.

In `@test/unit/explain.test.ts`:
- Around line 384-412: In the test case “28. An array literal is read member by
member, and only where it is one (`#225`)”, declare the repeated statement as a
single input variable before calling explainJson, pass that variable to
explainJson, and reuse it for the span-slicing assertions. Remove the duplicated
string literal while preserving all existing expectations.
🪄 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: 0df49f39-806a-4139-9f43-bdf2a1941bc1

📥 Commits

Reviewing files that changed from the base of the PR and between 26da4d0 and ad528dc.

📒 Files selected for processing (15)
  • commands/explain/README.md
  • commands/explain/examples.md
  • docs/MATRIX.md
  • package.json
  • scripts/explain-value-census.ts
  • src/explain.ts
  • src/explain/args.ts
  • src/explain/values.ts
  • src/index.ts
  • test/fixtures/explain/values.json
  • test/integration/chr.ts
  • test/integration/explain-values.test.ts
  • test/unit/explain-args.test.ts
  • test/unit/explain-values.test.ts
  • test/unit/explain.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 (9)
**/*.{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/integration/chr.ts
  • src/index.ts
  • test/unit/explain-args.test.ts
  • test/integration/explain-values.test.ts
  • test/unit/explain.test.ts
  • src/explain/values.ts
  • src/explain.ts
  • src/explain/args.ts
  • test/unit/explain-values.test.ts
  • scripts/explain-value-census.ts
docs/MATRIX.md

📄 CodeRabbit inference engine (AGENTS.md)

Read docs/MATRIX.md after the constitution; use it as the only command×protocol status surface and work on the highest-priority cell that is not CHR-passed.

Files:

  • docs/MATRIX.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:

  • docs/MATRIX.md
  • commands/explain/examples.md
  • commands/explain/README.md
commands/*/examples.md

📄 CodeRabbit inference engine (commands/AGENTS.md)

commands/<name>/examples.md must contain numbered, runnable examples, with example N corresponding to assertion N in test/integration/<name>.test.ts; these examples define what CHR-passed is measured against.

Files:

  • commands/explain/examples.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/examples.md
  • commands/explain/README.md
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/index.ts
  • src/explain/values.ts
  • src/explain.ts
  • src/explain/args.ts
test/integration/**/*.test.{ts,js}

📄 CodeRabbit inference engine (test/AGENTS.md)

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.

Files:

  • test/integration/explain-values.test.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
test/fixtures/**/*

📄 CodeRabbit inference engine (test/AGENTS.md)

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

Files:

  • test/fixtures/explain/values.json
🧠 Learnings (2)
📚 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/values.ts
  • src/explain/args.ts
📚 Learning: 2026-08-06T23:42:32.848Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 227
File: src/explain/args.ts:151-158
Timestamp: 2026-08-06T23:42:32.848Z
Learning: In the RouterOS explain lexer, use the shared `continuationLength(text, at)` helper for backslash-newline handling. Treat only `\n` and `\r\n` as continuations; a lone `\r` after a backslash must not be skipped. In `args.ts`, reject a lone-`\r` escape as invalid so token names and source spans remain accurate.

Applied to files:

  • src/explain/args.ts
🪛 OpenGrep (1.26.0)
test/integration/explain-values.test.ts

[ERROR] 429-431: 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] 455-457: 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] 514-514: 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] 533-533: 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] 547-547: 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] 565-567: 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] 593-593: 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 (16)
src/index.ts (1)

317-317: LGTM!

Also applies to: 389-389

scripts/explain-value-census.ts (2)

127-157: LGTM!

Also applies to: 159-184


100-123: 📐 Maintainability & Code Quality

No change needed for census shape selection.

scripts/explain-value-census.ts already uses src/explain/values.ts shape hints with the same braceArrays, allowBareString, and context rules as src/explain.ts.

package.json (1)

98-98: LGTM!

docs/MATRIX.md (1)

74-74: LGTM!

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

273-311: LGTM!

src/explain/values.ts (1)

12-32: LGTM!

Also applies to: 61-124, 167-245, 276-304, 316-319

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

66-113: LGTM!

Also applies to: 154-175, 271-279, 364-432, 434-518, 525-532, 541-629, 644-721, 842-845, 862-863

src/explain/args.ts (1)

134-160: LGTM!

Also applies to: 173-238, 251-251, 355-355, 374-375, 546-546, 757-968, 980-1028

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

178-180: LGTM!

Also applies to: 193-199, 217-223, 232-234

src/explain.ts (1)

96-96: LGTM!

Also applies to: 459-471, 1110-1149, 1564-1566

commands/explain/README.md (1)

548-548: LGTM!

Also applies to: 685-742

commands/explain/examples.md (1)

443-445: LGTM!

Also applies to: 500-544, 545-568

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

361-364: LGTM!

Also applies to: 413-424, 426-461

test/integration/explain-values.test.ts (1)

184-187: LGTM!

Also applies to: 329-340, 414-627

test/integration/chr.ts (1)

288-293: LGTM!

Comment thread commands/explain/README.md
Comment thread scripts/explain-value-census.ts
Comment thread scripts/explain-value-census.ts Outdated
Comment thread test/fixtures/explain/values.json
Comment thread test/unit/explain.test.ts
- README census figures were stale: the fixture and its assertions were
  regenerated after the comma-spelling change but the prose was not. Now
  19,709 / 13,168 / 6,541 / 570 / 814, matching `explain:value-census`.
  (Copilot ×2, CodeRabbit — the same real defect from three angles.)
- `pushArrayMembers` returned a bare boolean, so every withdrawal published
  "an array literal with an empty member" even when the fault was a `*`/`+`
  lead. It now returns the reason, and the two causes are named separately.
  (Copilot.)
- Deduplicate the interior fixture: three probe rows appeared twice because two
  rounds asked the same input. The builder keeps the first reading.
  (CodeRabbit.)
- Declare `name` on the census `Emitted` interface and drop both casts, so
  `keyedElements` is type-checked rather than asserted. (CodeRabbit.)
- Write census output with `Bun.write` instead of `console.log` before
  `process.exit`, which can truncate a piped stdout in Bun. (CodeRabbit.)
- Hoist the duplicated 74-character input literal in example 28's assertion.
  (CodeRabbit.)
- Fix the "Braces a command argument does take are…" fragment in both the
  README and the examples file. (Copilot ×2.)

Verified: `bun run lint:ci`, `bun test` (2,435 pass), `bun run build`, and
`test/integration/explain-values.test.ts` 3 pass / 0 fail on CHR 7.23.3.

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

mobileskyfi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Meta Muse Spark 1.2 - Review of PR #256 (value interior V1) — gaps & follow-ups

Reviewed main...agent/explain-value-interior (15 files, +2676). The interior slice is solid and well-grounded (61 member rows, CHR 7.23.3 IL + highlight + typeof). The items below are what it missed or left implicit — none block the V1 shape, each should be a follow-up or a doc fix before #239 S2 / #236 lock the span vocabulary.

1. braceArrays gate is path-only, not verb-aware

src/explain.ts:1120 and scripts/explain-value-census.ts gate with split.path === "/". The PR's own grounding table shows the device distinguishes verbs at the same path:

  • :put {1;2} / :local z {1;2} / :foreach i in={1;2} -> parse
  • :log info message={1;2} -> error at { (path is /log, so path alone happens to exclude it, but...)

:global, :set, :delay, :return, :error all resolve to / too. Are :set x {1;2} and :global y {1;2} valid array literals? Is :delay delay-time={1;2}? A sweep of all root builtins that resolve to / is missing. If the set is larger than {positional, "in"}, the current ARRAY_ARG_NAMES = {"in"} is incomplete; if it is exactly that, the path check is redundant and the verb/candidate matters.

Ask: enumerate :<builtin> value slots on CHR and either widen ARRAY_ARG_NAMES or narrow the gate to (path==="/" && verb in {"local","global","put","len","foreach",...}). The existing fixture already proves the negative cases are error-classed, so the positive set needs the same proof.

2. ARRAY_ARG_NAMES is deliberately tiny — needs a grounding note

// src/explain/args.ts:206
const ARRAY_ARG_NAMES: ReadonlySet<string> = new Set(["in"]);

Comment says "grounded and deliberately tiny". The grounding only cites :log info message vs in=. :for i from=0 to=5, :pick, :toarray, etc. have not been swept. This is fine for V1, but it should be an explicit follow-up (or expand the positions[] probe in values.json to cover every :<builtin> <name>={1;2}).

3. MAX_MEMBER_DEPTH = 8 is silent and undocumented

pushArrayMembers stops descending at depth 8 and keeps only array shape (src/explain/args.ts:893). No example, README line, or test exercises the truncation. Corpus never hits it (max nesting 1147 nested elements, but depth unknown), but :local z {{{{{{{{{1;2}}}}}}}}} would silently drop inner members with no defect. For an untrusted --file input this is a silent data loss.

Fix: document the bound in commands/explain/README.md (approx "nested beyond 8 deep is located as array without members") and add a unit test that members beyond 8 are absent but the outer array remains.

4. Nested invalid array not withdrawn

Top-level (1,) is handled (test expects withdrawal), but nested invalid literals like { (1,) } or {a=(1,)} slip through:

  • isArraySource("(1,)") -> false (trailing-empty check fails) -> not recognised as sourceShape: "array"
  • pushArrayMembers then falls to the literalValue branch and continue-abstains instead of returning EMPTY_MEMBER.

The parent { (1,) } therefore stays as array with one abstained member rather than being withdrawn. Device :local z {(1,)} is a syntax error. The same applies to {1;2,} trailing comma.

Fix: when a member value starts with ( or { but fails isArraySource due to empty-member, treat as invalid and withdraw the container (or explicitly document "nested invalid arrays are abstained, not withdrawn" if that is the intended contract).

5. {a=} / {a= } — silent abstention vs drop-= behaviour

pushArrayMembers:913 does if (key===null || value.start===value.end) continue; with comment "device drops the = and keeps the NAME as a string member, so neither reading is safe." The choice to abstain is safe, but it is not tested and not mentioned in examples.md 28. A call-site that expects name: "a" will get nothing. Worth a one-line doc + a values.json position row.

6. Census is re-derivable but not drift-gated

scripts/explain-value-census.ts fixes the #242 throwaway-script problem, but there is no explain:value-census:check (unlike explain:menus:check) and bun run lint:ci does not verify test/fixtures/explain/values.json#corpus matches the script's output. The numbers can drift silently again.

Fix: add "explain:value-census:check": "bun run scripts/explain-value-census.ts --check" and wire it into lint:ci, or at least note in commands/explain/README.md that the fixture is the output of bun run explain:value-census --json.

7. Illegal {…} in a command argument is still a silent abstention

PR body explicitly defers: "A brace array in a command argument is a device error... Left out deliberately." That is the right V1 call (error severity needs a per-attribute schema sweep), but the silent abstention hides a real user error (/ip/dns/set servers={1.1.1.1;8.8.8.8} looks like "no values" rather than "wrong delimiter"). Track as a follow-up on #225 or #239 so the diagnostic does not get lost. A values.details note or a structure.diagnostics[] candidate would be the eventual home.

8. Minor doc nits

What was done well (for the record)

  • One-sided scoring (nothing = abstain) in test/unit/explain-values.test.ts:372 is the right falsifier.
  • depthZeroOffsets shared walk prevents the two-scanner disagreement that has bitten this area before.
  • The 61 row interiorGrounding fixture is generated from probe JSON, not hand-typed.

Happy to re-review the follow-up that addresses (1)/(3)/(4)/(6).

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

The comments from Muse Spark suggest {a=} / {a= } but this highlights a related issue for this/future work. e.g. {a=1 where {<alpha>= is different than {1=1} so there is some type sensitivity here.

:put {1=1}
1=1
:put (1=1)
true
:put (1=)
syntax error (line 1 column 10)
:put (a=)
syntax error (line 1 column 10)
:put a=
syntax error (line 1 column 8)
:put {a=1}
a=1
[skyfi@bigdude] > :put {a=a}
a=
:put {1=a}
1=
:put {1.1=1.1}
1.1=1.0.0.1
:put {[:timestamp]=[:timestamp]}
false
:put {[:timestamp]=1}
false

The post-#256 review said `{(1,)}` slips through, and it does: a `(…)`
member is a GROUP as often as an array, so it fell to the fallback branch
and abstained while the enclosing literal kept its `array` shape. `:parse`
rejects the whole statement.

Grounded on CHR 7.23.3 (`.scratch/explain-225-nested-literal-probe.ts`,
25 literals) rather than on `highlight`, which is the permissive gate here
— it ACCEPTS `{1;2,}`, `{2,}` and `{(1,2),}`, all of which `:parse`
rejects. Eight spellings were fabrications: `{(1,)}`, `{a=(1,)}`,
`{1;(2,)}`, `{ (1,) ; 2 }`, `{()}`, `{a=()}`, `{(,)}` and `(1,(2,))`.
The nine accepted controls (`{(1)}`, `{a=(1)}`, `{1;(2)}`, `{{}}`,
`{1;{}}`, `{1;2;}`, `{{1;2};}`, `{1;2}`, `(1,2)`) still read.

The probe rows are committed as `interiorGrounding.nestedLiterals`, which
makes the previously dead `invalidLiterals` block's job an actual test:
rejected literals are scored one-sided (abstaining is fine, claiming a
shape is not). Corpus census is unchanged — this family has zero
occurrences in all 948 scripts.

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

Copy link
Copy Markdown
Contributor Author

Triaged all eight items against CHR 7.23.3 before answering, because two of the
review's device claims and one of my own round-5 readings turned out to be
wrong. Probes: .scratch/explain-225-review-round{5,6}-*.ts and
.scratch/explain-225-nested-literal-probe.ts.

One item was a real, shipped fabrication and is fixed here (e0d70ce).

Item 4 is half right. {(1,)} and {a=(1,)} do slip through — and so do six
more spellings the review did not name: {1;(2,)}, { (1,) ; 2 }, {()},
{a=()}, {(,)} and (1,(2,)). All eight came back pass with an array
shape over bytes :parse refuses. Root cause is the one the review identified:
a (…) member is a GROUP as often as an array ({(1)} is the one-member array
1), so it fell to the abstain branch and the container kept its shape. Now
parenMemberFault names the fault and withdraws the container.

The other half of item 4 is not a bug: {1;2,} was already withdrawn. The
review's reasoning ("isArraySource fails, so it abstains") is right about the
mechanism but the trailing-comma case reaches splitMembers through the
comma-array branch and returns EMPTY_MEMBER correctly.

highlight is not the oracle for this family. It ACCEPTS {1;2,}, {2,},
(1,) and {(1,2),}, every one of which :parse rejects. My round-5 probe
used it and produced four wrong rows; the committed fixture rows come from
:parse only. Worth knowing for anything downstream that treats a clean
highlight as proof a statement parses.

Item 3 is already testedtest/unit/explain-values.test.ts "member
descent is bounded, and the shape survives the bound" exercises 40 frames of
nesting and asserts 8 survive. The README did not say so; it does now.

Items 1, 2, 5, 6, 7 are real and filed, each with the device evidence:

  • explain: the brace-array gate is per (verb, slot), not per path #257 — the brace gate. The suspicion was right and understates it: the
    gate is wrong in both directions, and the rule is per (verb, slot), not
    per path. :delay {1;2}, :beep {1;2}, :resolve {1;2}, :if condition=,
    :local name= and ~30 more named slots are device errors centrs calls
    arrays; :for from=, :execute script=, :retry command=, :local value=
    and ~55 more are accepted braces centrs refuses. And "accepts" is not one
    outcome — :onerror in={1;2} and :retry command={1;2} lower to CODE
    BLOCKS, not arrays, while :foreach in={1;2} is an array, so the same name
    differs by verb. Root builtins for that sweep were enumerated from the
    device, not typed by hand (/console/inspect request=completion on ":").
    Blast radius today is 0: all 112 corpus brace arrays sit on :local/
    :global/:set/:foreach/:return/:execute/:put, all grounded as
    accepting.
  • explain: array members the device has and centrs drops ({a=}, comparison members, key types) #258 — the {a=} family (item 5) plus the maintainer's comment. The
    device does have those members: {a=} is one positional str member
    (not a keyed one, and not the variable reference a bare {a} would be), and
    {$a=1}/{(a)=1}/{[:timestamp]=1} are one bool each because =
    only binds a key when the left side is a name.
  • explain: a brace array in a command argument is a device error, not a silent abstention #259 — the command-argument brace diagnostic (item 7), which was already
    this PR's stated deferral.
  • explain: the value census is re-derivable but not drift-gated #260 — the census drift gate (item 6), with the reason a
    explain:value-census:check in lint:ci cannot work as proposed: the census
    reads a corpus that no CI job has, which is why explain:corpus-census has no
    :check either. The drift that actually happened was prose-vs-fixture and can
    be gated with no corpus at all.

Item 8's first bullet is a PR-body note, added. The census is unchanged by the
fix (this family has zero corpus occurrences), so no figures moved.

The eight fabrications and the nine accepted controls are now committed as
interiorGrounding.nestedLiterals and scored one-sided in a test — which also
gives the previously dead invalidLiterals block a job.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/explain-value-census.ts (1)

160-176: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Count only anchors that the strict lexer compares.

Line 162 counts every kept entry. Lines 163-164 skip every element. Array members therefore increase strictComparableAnchors without any strict-token comparison. This makes the metric and its zero-contradiction claim inaccurate.

Filter elements before incrementing and iterating. Regenerate the fixture counts and corpus prose after this correction.

Proposed fix
 			const strict = lexArguments(statement, split.argsAt);
 			if (strict.read) {
-				result.strictComparableAnchors += kept.length;
-				for (const entry of kept) {
-					if (entry.kind === "element") continue;
+				const comparable = kept.filter((entry) => entry.kind !== "element");
+				result.strictComparableAnchors += comparable.length;
+				for (const entry of comparable) {
🤖 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 `@scripts/explain-value-census.ts` around lines 160 - 176, Update the strict
comparison block around lexArguments so strictComparableAnchors counts only
non-element entries that are actually examined by the strict lexer. Filter out
entries with kind "element" before incrementing the counter and iterating, while
preserving the existing token and contradiction checks; then regenerate the
fixture counts and corpus prose.
🤖 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/args.ts`:
- Around line 925-928: Update the depth-limit handling in the member-reading
function to withdraw the enclosing array shape when depth >= MAX_MEMBER_DEPTH
instead of returning null, which callers interpret as successfully reading all
members. Add and use the proposed DEPTH_BOUND_REACHED withdrawal reason
alongside the existing withdrawal constants, preserving null for non-cap paths
where it represents the established successful completion behavior.

---

Outside diff comments:
In `@scripts/explain-value-census.ts`:
- Around line 160-176: Update the strict comparison block around lexArguments so
strictComparableAnchors counts only non-element entries that are actually
examined by the strict lexer. Filter out entries with kind "element" before
incrementing the counter and iterating, while preserving the existing token and
contradiction checks; then regenerate the fixture counts and corpus prose.
🪄 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: a05d8fd1-66b9-4303-91e2-9b57d71b4f39

📥 Commits

Reviewing files that changed from the base of the PR and between ad528dc and e0d70ce.

📒 Files selected for processing (7)
  • commands/explain/README.md
  • commands/explain/examples.md
  • scripts/explain-value-census.ts
  • src/explain/args.ts
  • test/fixtures/explain/values.json
  • test/unit/explain-values.test.ts
  • test/unit/explain.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
commands/*/examples.md

📄 CodeRabbit inference engine (commands/AGENTS.md)

commands/<name>/examples.md must contain numbered, runnable examples, with example N corresponding to assertion N in test/integration/<name>.test.ts; these examples define what CHR-passed is measured against.

Files:

  • commands/explain/examples.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/examples.md
  • commands/explain/README.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:

  • commands/explain/examples.md
  • 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.test.ts
  • scripts/explain-value-census.ts
  • test/unit/explain-values.test.ts
  • src/explain/args.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
test/fixtures/**/*

📄 CodeRabbit inference engine (test/AGENTS.md)

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

Files:

  • test/fixtures/explain/values.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/args.ts
🧠 Learnings (2)
📚 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/args.ts
📚 Learning: 2026-08-06T23:42:32.848Z
Learnt from: mobileskyfi
Repo: tikoci/centrs PR: 227
File: src/explain/args.ts:151-158
Timestamp: 2026-08-06T23:42:32.848Z
Learning: In the RouterOS explain lexer, use the shared `continuationLength(text, at)` helper for backslash-newline handling. Treat only `\n` and `\r\n` as continuations; a lone `\r` after a backslash must not be skipped. In `args.ts`, reject a lone-`\r` escape as invalid so token names and source spans remain accurate.

Applied to files:

  • src/explain/args.ts
🔇 Additional comments (8)
test/unit/explain.test.ts (1)

385-387: LGTM!

src/explain/args.ts (3)

804-840: LGTM!


929-1005: LGTM!


1048-1061: LGTM!

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

66-114: LGTM!

Also applies to: 711-758

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

894-1020: LGTM!

commands/explain/README.md (1)

718-733: LGTM!

commands/explain/examples.md (1)

501-568: LGTM!

Comment thread src/explain/args.ts
Comment thread src/explain.ts Outdated
Comment thread test/integration/explain-values.test.ts
…257)

Closes the blocking review thread on #256: the `split.path === "/"` gate
admitted `:delay {1;2}`, `:beep {1;2}` and `:resolve {1;2}` — CHR 7.23.3
syntax errors this PR was newly publishing member facts under — while
refusing accepted slots like `:for from=` and `:local value=`.

The gate is now keyed on (verb, slot), from a sweep of every root builtin
(enumerated from the device, not by hand) and every slot it completes:
222 rows in `interiorGrounding.braceSlots`, baked into
`src/explain/brace-slots.ts`, with a unit test asserting the table still
equals the fixture's `array` rows.

A slot has FOUR outcomes, and `{1;2}` cannot see the difference — `:local
z {1;2}` and `:execute script={1;2}` both lower to `…=1;2`. `{(1,2)}`
can: an evaluated slot lowers it to `(, 1 2)`, a script slot echoes
`(1,2)` verbatim. Reading only `{1;2}` had put `:execute script=` in the
array set, one step from hinting `array` on a script body. So: 72 array,
21 code, 7 text, 92 error, 30 unaskable — and only the 72 are read.
Positionals are keyed by index, because `:local {1;2}` is the NAME slot
and does not parse while `:local z {1;2}` does.

Also from this round:

- The member-descent bound withdraws instead of keeping an unverified
  shape (CodeRabbit). A `(1,)` buried nine levels deep is a syntax error
  the cap made invisible: `pass` + `array` on a statement `:parse`
  refuses. Deepest corpus member is 6, so this costs nothing seen.
- `strictComparableAnchors` counts only the entries it compares
  (CodeRabbit, outside-diff). Measured no-op — a statement the strict
  lexer reads has 0 elements across all 948 scripts — but the counter now
  matches its published definition.

Example 28c and its CHR assertion cover the accepted/rejected controls,
the nested rejection, and the depth bound; `test:integration` for this
file is 284 device assertions. Census moves by exactly one array
(813, the `:execute {…}` script body), regenerated in the fixture, the
assertions and the README prose.

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

Copy link
Copy Markdown
Contributor Author

All four items from this round are fixed in f376a3d, and the blocking one
turned out to be blocking for a sharper reason than I had priced.

The gate is now keyed on (verb, slot), fail closed. Root builtins were
enumerated from the device and every slot each one completes was probed —
222 rows, committed as interiorGrounding.braceSlots, baked into
src/explain/brace-slots.ts, with a unit test asserting the table still equals
the fixture's array rows. :delay {1;2}, :beep {1;2}, :resolve {1;2},
:local {1;2}, :local name={1;2}, :if condition={1;2} and
:onerror e in={1;2} no longer produce an array occurrence; :for from=,
:local value=, :set g {…} and 60-odd more now do.

A slot has four outcomes, not three, and my first sweep got one wrong.
I classified on {1;2} and put :execute script= in the array set. It is not:
:execute script={(1,2)} lowers to script=(1,2) verbatim while
:local z {(1,2)} lowers to (, 1 2). The slot takes script TEXT, and {1;2}
cannot see the difference because both spellings lower to …=1;2. Caught by
running the corpus census after the change and asking why the array count went
UP by one — the one was a :execute { :put [...]; } script body about to be
hinted array. The re-sweep uses a literal whose evaluated and verbatim
readings differ: 72 array, 21 code, 7 text, 92 error, 30 unaskable, and only
the 72 are read. #257 updated with the leftovers.

The depth cap was a real fabrication and is fixed. {{{…{(1,)}…}}} at
depth 9 came back pass with an array shape over bytes :parse refuses —
the same class this PR exists to remove, hidden below the cap. The bound now
withdraws with a named reason rather than returning "all members read". The
cost is a valid literal nested deeper than eight, which is 0 in the corpus:
the deepest member across 948 scripts sits at depth 6, and I checked the device
has no shallow limit of its own (valid nesting parses to at least 64, and the
buried fault is rejected at every depth). The existing bound test and the
README sentence both moved with it.

CHR coverage added as example 28c: the accepted and rejected slot controls,
the array-vs-script discriminator, the nested rejections with their group
neighbours as controls, and the depth bound — each asked of the device first,
then scored against what centrs claims for the same bytes.
test/integration/explain-values.test.ts is 284 device assertions, and the
full bun run test:integration is 146 pass / 0 fail on 7.23.3.

strictComparableAnchors (outside-diff): applied, and it is a measured
no-op. Elements can only appear where the strict lexer read the statement, and
a structured value makes that lexer refuse — 0 elements across all 948 scripts,
so the published figure was never inflated. The counter now matches its
definition rather than depending on that staying true.

The census moved by exactly one array as a result of the text reclassification
(813, was 814); the fixture, the assertions and the README prose were all
regenerated together this time.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Muse Spark 1.2 — Re-review of #256 @ f376a3d

Re-read main...agent/explain-value-interior after the three fixup commits (f126699, e0d70ce, f376a3d). The slice is now verb+slot-gated, depth-withdrawing, and nested-fault-withdrawing — the three blocking gaps from the first review are closed. No blocking issue remains from here; below is what was verified and the only nits left (all track as follow-ups or Not done here).

Verified fixed

1. (verb, slot) gate — closed in f376a3d (#257)

  • src/explain/brace-slots.ts bakes the 222-row CHR sweep (interiorGrounding.braceSlots) as Map<verb, Set<slot>> with the 4-way outcome model (array / code / text / error) and the inconclusive → absent → refuse fail-closed rule documented in the module header. 30 inconclusive rows stay absent, costing coverage not correctness.
  • src/explain/args.ts:224-239 braceOpensArray now keys on (verb, slot) with positional #<index> handling (:local {1;2}#0 refused, :local z {1;2}#1 accepted) and braceValueOwner lower-casing the attribute name.
  • src/explain.ts:1112-1116 and scripts/explain-value-census.ts:102-103 thread directiveVerb only when path === "/", i.e. a root scripting directive — :log info message={1;2} (/log) correctly never opens the gate.
  • test/unit/explain-values.test.ts asserts the baked table still equals the fixture's array rows; test/integration/explain-values.test.ts 28c re-asks every accepted and rejected control on CHR (:local/ :global/ :put/ :return/ :foreach in=/ :for from= vs :delay/:beep/:resolve/:local name=/:if condition=/:onerror/:retry/:execute script=//ip/dns/set servers=) via :parse IL ((, 1 2) vs =1;2 disambiguator) and scores claimsArray against it. Good.

2. Depth bound now withdraws

  • src/explain/args.ts:199 MAX_MEMBER_DEPTH = 8, 816-817 DEPTH_BOUND_REACHED, 936-943 early-return now returns the withdrawal reason instead of null. Callers in pushArrayMembers and lexValueAnchors:1077-1080 truncate and complete: false. commands/explain/README.md:756-761 documents eight frames → withdraw, deepest corpus is 6. Unit + integration 28c covers nest(9,"(1,)") withdraws vs nest(8,"1") reads vs nest(9,"1") withdraws. The previous fabrication is gone.

3. Nested invalid members withdraw the container

  • src/explain/args.ts:813-851 parenMemberFault names EMPTY_GROUP vs EMPTY_MEMBER (empty group {( )}/{a=()} vs empty comma member {(1,)}/{(,)}/{1;(2,)}/(1,(2,))) and pushArrayMembers:978-993 propagates it. isArraySource still only recognises comma-arrays; groups like {(1)} stay single-member arrays via the fault path, not via shape. Integration 28c covers the 7 rejected and 4 accepted neighbours.

4. Other first-review fixes confirmed

  • args.ts:913 {a=} comment + continue abstention left intentional (device drops = and keeps a as str — neither reading is safe). Noted but not a regression.
  • scripts/explain-value-census.ts:105-186 Emitted.name? + comparable = kept.filter(kind !== "element") + await Bun.write drain fix all landed and match the corpus block (valueOccurrences 19,708 · elementOccurrences 5,636 · keyed 529 · nested 1,147 · strictComparable 13,168 · recovered 6,540 · unread 569 · boundary 0 · invalid 0 · dangling 0 · containment 0 · arrays 813).
  • Doc drift commands/explain/README.md:781-791 and scripts/explain-value-census.ts:241-250 numbers now match bun run explain:value-census --json and the fixture. bun run lint + tsc --noEmit green.

Remaining — none blocking, track as noted

a. Directive verb casing

  • braceSlotTakesArray and braceOpensArray compare verb verbatim. verbsplit preserves RunToken.name casing, so :LOCAL z {1;2} would miss the local entry while the device treats it as local. No corpus hit and no test exercises it. If case-insensitive directives are in scope, normalise verb.toLowerCase() at the gate (and keep the table lower-cased as it is). Worth a one-liner follow-up or an explicit "directive verbs are lower-cased by the split" invariant if they already are — I didn't find the lower-casing.

b. Census drift gate

  • bun run explain:value-census makes the 19,708/6,540 numbers re-derivable, but bun run lint:ci still does not gate values.json#corpus against it (noted in PR body as explain: the value census is re-derivable but not drift-gated #260). No action here, just flagging that the re-derivability can still drift silently until the explain:value-census:check wired into lint:ci lands.

c. Illegal {…} in a command argument stays a silent abstention

No other functional contradiction found. The (verb, slot) table, the withdrew-on-depth contract, and the nested-fault withdrawal together with 28/28b/28c CHR coverage make this safe to land for #225 V1.

…225)

The re-review flagged that `braceSlotTakesArray` compares the verb
verbatim while `verbsplit` keeps source casing, and suggested
normalizing with `toLowerCase()`. The device says not to: `:LOCAL z
{1;2}`, `:Local z {1;2}`, `:PUT {1;2}` and `/IP/DNS/set
servers=1.1.1.1` all draw `expected command name (line 1 column 2)` on
CHR 7.23.3. Lower-casing at the gate would open it on a statement that
does not parse — the fabrication class this table exists to remove.

So the behavior is already right and stays; what was missing is that it
was an accident rather than a stated rule. Now it is written in the
module header and in example 28c, with unit and CHR assertions on both
halves: a differently-cased DIRECTIVE gets no array, while an
argument's casing is free (`:local Z {1;2}` parses and binds `$Z`).

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

Copy link
Copy Markdown
Contributor Author

Thanks — nit (a) is worth having, but the suggested fix is the wrong way round,
so dfa5925 pins the invariant instead of normalizing.

Directive casing. The observation is right — verbsplit keeps source
casing and braceSlotTakesArray compares verbatim — but verb.toLowerCase()
at the gate would be a fabrication. RouterOS accepts only the lower-case
spelling, and it is not a menu-name nicety:

:parse ":LOCAL z {1;2}"          -> expected command name (line 1 column 2)
:parse ":Local z {1;2}"          -> expected command name (line 1 column 2)
:parse ":PUT {1;2}"              -> expected command name (line 1 column 2)
:parse "/IP/DNS/set servers=…"   -> expected command name (line 1 column 2)
:parse ":local Z {1;2}"          -> (evl /localname=$Z;value=1;2)      argument casing is free

So :LOCAL z {1;2} is a statement the device refuses to parse, and normalizing
the verb would open the gate and hint array on it — the exact class the table
exists to remove. Matching verbatim gives the device's answer for the device's
reason.

What was genuinely missing is that this was an accident rather than a stated
rule, with nothing exercising it. Now it is in the brace-slots.ts header and
in example 28c, with both halves asserted in unit and on CHR: three
differently-cased directives get no array, and :local Z {1;2} still does.
test/integration/explain-values.test.ts is 292 device assertions.

(b) census drift gate — tracked as #260, which also records why the
explain:value-census:check in lint:ci shape cannot work as first proposed:
the census reads a corpus that no CI job has, the same reason
explain:corpus-census has no :check. The drift that actually happened was
prose-vs-fixture, which needs no corpus, and that is the option to build first.

(c) command-argument diagnostic — tracked as #259, still blocked on the
per-attribute sweep, and your framing of the risk is in the issue: the 28c
negative controls currently read as "no values" to a caller rather than "wrong
delimiter".

@mobileskyfi
mobileskyfi merged commit c3e295a into main Aug 11, 2026
10 checks passed
@mobileskyfi
mobileskyfi deleted the agent/explain-value-interior branch August 11, 2026 00:15
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