Skip to content

feat(devices): canonical Zod device-record schema + mutation-path validation (JG-06)#50

Merged
mobileskyfi merged 2 commits into
mainfrom
feat/devices-zod-schema-jg06
Jun 16, 2026
Merged

feat(devices): canonical Zod device-record schema + mutation-path validation (JG-06)#50
mobileskyfi merged 2 commits into
mainfrom
feat/devices-zod-schema-jg06

Conversation

@mobileskyfi

Copy link
Copy Markdown
Contributor

What

JG-06 (Stage 4): a canonical Zod model for a WinBox-CDB device record — the
typed source of truth, living outside prose and the CDB key/value codec, with the
CDB as one rendering of it.

  • src/devices-schema.tsdeviceRecordSchema + DeviceRecord type +
    parseDeviceRecord() (throws a cdb/invalid-record CentrsError). It is
    permissive about target/profile formats (a target may be IPv4/IPv6/MAC,
    optionally host:port; a profile may be a <none>/own sentinel) and
    lenient about recordType — the CDB decoder preserves record types centrs
    does not name yet (recordTypeNameunknown(N)), so the canonical model
    must round-trip them. The strict record-type name check stays at the add
    CLI boundary where a human types a name; isKnownRecordType / recordTypeNames
    are exported for that.
  • Mutation-path validationaddDevice/setDevice validate the logical
    record before rendering to CDB bytes, so a blank target or wrong field type
    fails with an actionable error for every caller (CLI, MCP, direct API, and
    a future JSON/YAML import), not just the CLI's own guards. setDevice stays
    lenient on the inherited recordType/target, so editing an unusual
    existing record never regresses.
  • Cataloged cdb/invalid-record and regenerated docs/errors/cdb/invalid-record.md.

Scope

Device-records-first, per the plan — settings and the read/decode path are
deliberately out of scope until this layer stays clean. This lays groundwork for
the deferred devices-edit TUI (JG-22) and a JSON/YAML device representation (JG-19).

Why no CHR

The device registry is the local WinBox CDB (file I/O), not a router. No
transport/RouterOS path is touched, so this is not CHR-gated (the network-free
cli-smoke tier already covers devices add/list/remove).

Verification

bun run lint + bun run lint:ci + bun run test (710 pass / 0 fail / 26 skip)

  • bun run build all green.

🤖 Generated with Claude Code

…idation (JG-06)

Extract a typed source-of-truth for a device record, outside prose and the CDB
key/value codec, with the CDB as one rendering of it:

- src/devices-schema.ts: `deviceRecordSchema` (Zod) + `DeviceRecord` type +
  `parseDeviceRecord()` (throws `cdb/invalid-record` CentrsError). Permissive on
  target/profile *formats* and lenient on `recordType` — the decoder preserves
  types centrs does not name yet (`recordTypeName` → `unknown(N)`), so the model
  must round-trip them; the strict record-type *name* check stays at the `add`
  CLI boundary. `isKnownRecordType`/`recordTypeNames` exported for that check.
- addDevice/setDevice validate the logical record before rendering to CDB, so a
  blank target or wrong field type fails for every caller (CLI, MCP, direct API,
  future JSON/YAML import) — not just the CLI's own guards. setDevice stays
  lenient on the inherited recordType, so editing an unusual existing record
  never regresses.
- Cataloged the new code + generated docs/errors/cdb/invalid-record.md.

Device-records-first per the plan; settings + the read/decode path are out of
scope until this stays clean. Groundwork for the devices-edit TUI (JG-22) and a
JSON/YAML device representation (JG-19). Local CDB only — no CHR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 16, 2026 17:15
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@mobileskyfi, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 33 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56e06f13-27d8-4680-9661-ed75f0341864

📥 Commits

Reviewing files that changed from the base of the PR and between d0f60f0 and 05d5f29.

📒 Files selected for processing (6)
  • docs/errors/cdb/invalid-record.md
  • src/core/error-catalog.ts
  • src/devices-schema.ts
  • src/devices.ts
  • test/unit/devices-mutate.test.ts
  • test/unit/devices-schema.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/devices-zod-schema-jg06

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 and usage tips.

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 canonical, Zod-backed schema for WinBox CDB device records and wires it into the device mutation path so invalid records fail consistently with a cataloged CentrsError.

Changes:

  • Introduces deviceRecordSchema + parseDeviceRecord() to validate logical device records and throw cdb/invalid-record.
  • Applies schema validation in addDevice and setDevice before rendering records to CDB bytes.
  • Registers the new error code in the catalog and scaffolds its docs page; adds unit tests for the schema.

Reviewed changes

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

Show a summary per file
File Description
test/unit/devices-schema.test.ts Adds unit coverage for the new schema and error behavior.
src/devices.ts Validates mutation inputs via the canonical device-record schema before CDB encoding.
src/devices-schema.ts Adds the canonical Zod schema, helpers (isKnownRecordType, recordTypeNames), and parseDeviceRecord() error mapping.
src/core/error-catalog.ts Registers the new cdb/invalid-record error code.
docs/errors/cdb/invalid-record.md Adds the required docs stub for the new error code.

Comment thread src/devices-schema.ts
Comment on lines +65 to +74
function remediationFor(field: string): string {
switch (field) {
case "target":
return "Pass a non-blank address or MAC (optionally host:port) as the target.";
case "recordType":
return `Record type must be a non-negative integer; the named types are ${recordTypeNames.join(", ")}.`;
default:
return "Check the device-record field types: strings for user/password/session/comment/group/profile, a boolean for savedPassword.";
}
}
Comment thread src/devices-schema.ts
Comment on lines +90 to +95
throw new CentrsError({
code: "cdb/invalid-record",
summary: `Invalid device record: ${field} — ${issue?.message ?? "failed validation"}.`,
remediation: remediationFor(field),
context: { field },
});
Comment thread src/devices.ts
Comment on lines +1203 to 1207
// Validate the logical record against the canonical schema before rendering
// it to CDB bytes — a blank target or wrong field type fails here for every
// caller (CLI, MCP, direct API), not just the CLI's own guards.
const validated = parseDeviceRecord({
recordType: args.recordType ?? winBoxCdbRecordType.ipAdmin,
…se, wiring tests

- parseDeviceRecord: a non-object input (empty Zod issue path) now blames the
  whole record with a `(record)` remediation pointing at recordType + target,
  and the flattened Zod issues ride `causeData` (summarized, not raw) so schema
  failures are debuggable beyond the human summary.
- Add mutation-path wiring tests (devices-mutate.test.ts): addDevice rejects a
  blank target and setDevice rejects a wrong-typed field, both `cdb/invalid-record`
  — guarding the gate from accidental removal. Plus a schema test for the
  non-object `(record)` + causeData path.

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

Copy link
Copy Markdown
Contributor Author

@coderabbitai please review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details and files changed
gh pr view 50 --repo tikoci/centrs --json title,body,files,additions,deletions,baseRefName,headRefName

Length of output: 3021


🏁 Script executed:

#!/bin/bash
# Get the diff for the PR
gh pr diff 50 --repo tikoci/centrs

Length of output: 14491



@mobileskyfi This is a clean, well-scoped implementation. The schema design decisions — lenient recordType for round-tripping decoded records, strict non-blank target, Zod issues preserved in causeData — are all sound, and the test coverage is thorough. One inconsistency worth addressing:


🔴 addDevice does not validate comment through the schema gate

In setDevice, comment is included in the parseDeviceRecord(...) call, so a wrong-typed comment is caught. In addDevice, comment is built/processed separately and then passed directly to buildWinBoxCdbEntryRecord, bypassing the schema entirely.

// setDevice — comment IS validated ✓
parseDeviceRecord({
    ...
    comment,          // included
    ...
});

// addDevice — comment is NOT in the parseDeviceRecord call ✗
const validated = parseDeviceRecord({
    recordType: ...,
    target: ...,
    user: ...,
    // comment absent here
    group: ...,
});
const record = buildWinBoxCdbEntryRecord({
    ...
    comment,          // passed directly, not from validated
    ...
});

A non-CLI caller (MCP, future JSON/YAML import) passing comment: 42 (a number) to addDevice will not be caught by the schema gate. The fix is to include comment in the parseDeviceRecord(...) call in addDevice and use validated.comment in the buildWinBoxCdbEntryRecord call (matching the setDevice pattern).


Minor observations (non-blocking)

  • setDevice discards the return value of parseDeviceRecord — purely used for its validation side-effect, since romonAgent, fieldOrder, declaredFieldCount, and extraFields are correctly kept out of the schema. The intent is clear but an inline comment noting this explicitly ("validation only — schema omits CDB-internal fields") would help future readers.

  • docs/errors/cdb/invalid-record.md stub — the "will be expanded" note is self-documenting; fine as-is.

  • Only the first Zod issue surfaces in the error message (rest in causeData) — this is a reasonable deliberate UX choice, no change needed.

@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 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.

@mobileskyfi mobileskyfi merged commit 97d28b2 into main Jun 16, 2026
9 checks passed
@mobileskyfi mobileskyfi deleted the feat/devices-zod-schema-jg06 branch June 16, 2026 21:06
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