All notable changes to this repository should be recorded here.
The product evolves as a stream of changes; there are no alpha gates.
Per-feature status is tracked in docs/MATRIX.md. Use this file for
documenting cross-cutting shifts that affect contributors and consumers.
- Local Git gates now stop at the useful feedback boundary. Pre-commit runs
the near-instant Biome check; pre-push runs the same
lint:cirepo-check gate as GitHub (source/type checks, generated-doc drift, Markdown, spelling, and secrets) without serially duplicating the required unit-test and build jobs. The hosted repo-check job no longer runs its source subset twice, while unit, build, and stable CHR smoke coverage remain unchanged. The three bounded-traversal tests identified in #277 also have a 30-second runner budget so host contention does not turn their structural bounds into a laptop-speed assertion.
-
explainreads the array members an=makes, instead of abstaining at the sign. #256 read a{…}literal member by member but dropped every member whose=did not bind a key, which is three readings the device has:{a=}is not an empty-valued key but a positionalstrholding the name (the device drops the sign — and{1.1=}is the string1.1, not the address),{$a=1}/{(a)=1}/{[:timestamp]=1}are comparisons and therefore oneboolmember, and a leading-belongs to the key ({-1=1}). A key binds only where the name TOUCHES the sign, so{a =1}compares and{a=1}does not.boolis claimed only where the=is the member's top operator —{a b=1}and{$a=1,2}nest it under an operator whose type is its operands', and both still abstain — and an empty side, a syntax error in all 14 spellings asked, now withdraws the literal instead of leaving it reported as an array. That withdrawal keys on positive evidence that the left side is an expression, never on the key reader failing to recognize it, because the member-key grammar is not the identifier grammar and was swept rather than assumed:.,-and/are name bytes anywhere ({.id=1},{.=1},{a/b=1}),_is not ({a_b=1}compares), and@is not a member character at any position. Two member faults that predate the=axis are checked with it: a$takes an alphanumeric or a quote and nothing else ($"a b"is a name,$.idand$_aare syntax errors), and no run may end on a dangling operator ({a-},{$a.}). Grounded on CHR 7.23.3 across ten probe rounds; the 186 device rows areinteriorGrounding.keyBindingintest/fixtures/explain/values.jsonand are scored one-sided (abstention always allowed, a contradicted type never) by a new unit test. The value census is unchanged: no statement in the 948-script corpus reaches any of these spellings (#258, part of #225). -
LICENSE— centrs is MIT. The repository carried noLICENSEfile and the published@tikoci/centrstarball carried no license text, so GitHub reportedlicenseInfo: nulland consumers had only a bare"license": "MIT"string inpackage.jsonto go on — no grant they could actually read, quote, or redistribute, which is a problem for anything bundling centrs (a VS Code extension, say) (#208). The file now exists to back the declaration, and is in the npmfilesallowlist so it ships. -
bun run corpus:fetch— theexplaincensuses run from a bare clone and in CI.scripts/corpus-pin.jsonrecords which snapshot of the RouterOS script corpus centrs measures against (a commit intikoci/lsp-routeros-tsplus the blob's sha256);corpus:fetchdownloads it to a gitignored cache and verifies the hash, and a mismatch is a hard error rather than a silent re-download.explain:corpus-censusandexplain:value-censuspreviously resolved the corpus only as a sibling checkout, so they ran on one machine and never on a PR; both now fall back to the pinned snapshot and announce the source and hash they used. A new CI job gates the fetch and posts both censuses to the run summary (#186). -
The
explainvalue census is drift-gated, not just re-derivable. Its figures live in three places — the census itself, thecorpusblock oftest/fixtures/explain/values.json, and prose incommands/explain/README.md— and #256 shipped with the README still quoting pre-change numbers, caught by review rather than CI. Each link now has a gate keyed to the data it needs:bun run explain:value-census:checkre-runs the census against the committed fixture in the CI corpus job, while the README paragraph became a generated projection of that fixture (bun run explain:value-census:readme) checked bybun run explain:value-census:readme:checkinlint:ciand inbun test, needing no corpus at all. The generated prose reports the invariants as counters rather than asserting they hold, so regenerating after a regression states it instead of reading as reassurance (#260).
Patch release. @tikoci/centrs@0.1.3.
apicommand over rest-api + native-api isCHR-passed.centrs apibrings agh api-style structured passthrough to both transports, and folds the old standalonestreamcommand intoapi --stream(native-api only,--listenalias): aNativeApiSession.listen()reader with/cancelsupport emits NDJSON frames plus a run summary. Grounded against real CHR 7.23.1.- Multi-target fan-out is uniform across
api,retrieve,execute, andtransfer. A sharedexpandCdbSelection+runFanout+fanoutExitCodecore (extracted once, consumed by all four commands) lets any command take a CDB selector and run over every matching device. The positional boundary that ends the selector differs per command (final positional forapi/retrieve,--forexecute, the verb keyword fortransfer);terminalandapi --streamexplicitly rejectN > 1selections since neither has a sane multi-target framing. settingscommand (print/get/set/reset). A managed-key registry for the realCENTRS_*environment keys backs an atomiccentrs.envwriter:setvalidates per key, canonicalizes booleans to1/0on disk, warns (never blocks) on consequential values (insecure=true,transfer-via=ftp), and never partially writes on a validation failure;resetdeletes the managed line rather than blanking it (a blank value would itself mean "set to empty" for a bash-sourced file) and leaves foreign lines/comments untouched. Barecentrs settingsbehaves likeprintregardless of TTY. Four new error/warning codes (settings/reserved-key,settings/unknown-key,settings/consequential-value,settings/skip-env-file-active) plusinternal/settings-failed, all cataloged.CHR-passedvia 32 fixture-backed integration examples.- Device GPS: lat/lon/altitude storage +
--near/--bboxgeo predicates.centrs devicesnow stores GPS coordinates per device (lat-first ordering, chosen so agents guess the field order correctly) and supports proximity (--near) and bounding-box (--bbox) selection predicates.CHR-passedvia fixture-backed integration coverage. - Registry⇄MATRIX drift guard + "did you mean?" flag errors. A CI-checked
guard keeps the command/protocol registry and
docs/MATRIX.mdfrom silently drifting apart; unrecognized CLI flags now suggest the closest known flag instead of a bare rejection. - GitHub Pages: published API reference + error catalog.
typedocoutput and the generated error-code catalog now publish to GitHub Pages on push.
- Windows unit-test portability for
settingsand MNDP. Path-separator and related platform assumptions in thesettingsand MNDP unit suites are now Windows-safe (#170). - A real CodeQL-caught ReDoS in the device GPS geo-predicate parser, fixed rather than suppressed.
- CodeQL house-rules pack promoted to the default run, alongside CodeRabbit review-tone tuning and a reduced GitHub-checks timeout (15 → 7 minutes).
Patch release. @tikoci/centrs@0.1.2.
btestTCP multi-connection fan-out isCHR-passed.centrs btest --connection-count Nnow realizes the full fan-out: the client reads the session token from the primary's OK handshake and opensN-1additional TCP data connections, each sending the grounded secondary-join ([token:u16 BE][0x02][0…]). All connections drive into one sharedBandwidthCountersso throughput aggregates. Wire format confirmed byte-for-byte against RouterOS 7.23.1 (the server sends a 4-byte HELLO per connection then waits for all to join before streaming — sequential opens must not block on a reply, or the first secondary deadlocks the server). Unauthenticated TCP fan-out works end-to-end; authenticated (EC-SRP5) sessions stay single-stream and warn when the realizedactiveConnectionsfalls short of the requested count.BtestRunSummary/BtestClientDatagain anactiveConnectionsfield. Grounded via unit tests (secondary-join byte format, loopback fan-out) and integration example 11 on real CHR (4 connections open, data flows on each).
btestUDP client receive reported 0 bps. The UDP socket was bound withconnect()(filtering by remote address/port), which dropped the bandwidth-server's probe packets because they arrive from the server's data port, not the control port. The socket is now unconnected;--remote-udp-tx-sizeemits a warning when unset (needed by the server-push direction only). UDPreceiveandbothcycles are now CHR-gated on real RouterOS (rx ≈ 474 KB in CI).btest --connection-countwas silently ignored. The count was validated and accepted but never encoded into command byte 3 of the client hello, so RouterOS always sawcount=0and opened a single stream. The value is now threaded throughBtestClientSessionOptions→clientHandshake. Arouteros/btest-connection-count-single-streamwarning fires forconnection-count > 1sessions that fall back to one stream (e.g. EC-SRP5 auth).btest direction=bothTX was unbounded, starving RX. In TCPboththe client ran no status reader, so TX had no feedback, saturated the link, and collapsed server→client RX. ThetcpRxLoopnow demuxes embedded 12-byte status frames in-place by their structural marker (0x07 ?? 00 00) and passes them through the sharedapplyStatusFeedback/adaptTxFromStatusgate used bytcpStatusReaderLoop. A per-frame plausibility guard (CPU byte ≤ 100; sane sequence ceiling) filters false matches from--random-databulk streams. Grounded: a CHRbothrun fires 5 feedback events (server bytesReceived 78840 → 643860) and paces TX; RX is sustained instead of collapsing.
Patch release. @tikoci/centrs@0.1.1.
- CI is now faster on PRs. Cross-platform unit tests (macOS + Windows) moved
from
ci.yamltoqa.yaml, where they run in parallel with the CHR matrix on every push tomain. PRs no longer wait for macOS/Windows runners; escapes are caught on the merge-to-main QA run instead.
- CLI: registry-aware
missing-routertips. All router-taking commands now emit a CDB-aware tip when no<router>argument is supplied — listing saved handles from the CDB (tip/select-target) or pointing tocentrs discover --savewhen the registry is empty (tip/no-devices). discover --savelookup keys and progress. The saved device record now includesidentity=andmac=keys so downstream commands can resolve the entry; MAC addresses are de-duplicated. TTY progress is shown during a scan and a--savetip is printed at the end of a bare discover run.- CodeQL / security hardening (PR #80 review). Addressed reviewer findings:
clear-text credential log suppressed; progress output switched to stderr/JSON-safe
path;
retrieveresource leak closed; doc corrections.
First published pre-release (odd minor → npm next). @tikoci/centrs@0.1.0.
-
btest / clientis nowCHR-passed. A direct centrs-client → CHR/tool/bandwidth-servergated test (test/integration/btest-client.test.ts) boots a CHR with a host→guesttcp:2000forward and runs the centrs client against real RouterOS: unauthenticated TCP receive, an EC-SRP5 client proof verified by RouterOS's own server verifier, and a wrong-password reject (CHR 7.23.1). This closes the last open product-grid cell — the client cell was previously grounded only transitively (loopback + the server test). The integration harness (test/integration/chr.ts) gains anextraPortsoption for arbitrary host→guest forwards. UDP client→server and TCPconnection-count > 1fan-out remain loopback/transitive (README, Open questions). -
CI/release rework — staged gate, definitive QA matrix, NPM publish, extended sweep. The CI surface is reconciled with the tier/versioning doctrine (quickchr's
ci/publish/verify-extendedscheme as the reference).ci.yamlis a staged push/PR gate (lint ‖ unit+coverage → stable CHR smoke → cross-platform unit) with coverage + failing tests surfaced to job summaries and artifacts.qa.yamlis the definitive RouterOS channel matrix (push + weekly + dispatch +workflow_call) with event-aware concurrency (a new main push cancels the old run; dispatches never cancel one another — including anall-channels dispatch in a single run) and abun:sqliteresults store.codeql.yamlcarries security/quality scanning on its own cadence.release.yamlpublishes to npm on av*tag (even/odd minor →next/latest,--provenance, dry-run) — requires theNPM_TOKENrepo secret.verify-extended.yamladds a dispatch-only macOS-x86 / Windows-x86 sweep. Newchr-smokeintegration test +test:integration:smokescript;.coderabbit.yamlstages bot review to conserve credits. -
QA recency-aware channel matrix + cross-run history + must-pass gate.
qa.yaml's channel axis is now resolved per run by aresolve-matrixpre-flight job (scripts/qa-active-channels.ts) instead of a hard-coded list: it asks quickchr 0.4.2's public version/channel API (resolveAllVersions/selectActiveChannels, with the suffix-awarecompareRouterOsVersionthat finally orders7.24beta2 < 7.24rc1 < 7.24) for the channels worth booting — the released channels (stable, long-term) always, plus any pre-release (testing, development) at or ahead of stable. This closes thetestingblind spot and auto-adapts as MikroTik promotes builds, because the four channels are not monotonically ordered (a staletestingrc is skipped; it is picked up automatically once it leapfrogs stable). A newaccumulate-and-gatejob appends each CHR run to a durable append-log on theqa-historybranch (per-run artifacts have finite retention, and the channel→version drift over time is what a long history captures) and fails the run only when a released channel (stable, long-term) regresses — pre-release legs are best-effort, so a beta btest/EC-SRP5 flake (JG-31) is recorded but never reds main. Boundary: quickchr owns recency ("what's worth booting"); centrs owns the gate ("what must pass") — the must-pass policy lives once inscripts/qa-results-db.ts(channelPolicy/evaluateMustPassGate, mirrored by the matrixcontinue-on-error), with the cross-run accumulator inscripts/qa-history.ts. -
transfer / ssh(sftp) — SSH lands transfer-first. SSH joins centrs as a self-contained SFTP transfer client (src/protocols/sftp.ts) over the host OpenSSHsftpsubsystem — the only reliable SSH file path, since RouterOS's SSH server has no exec channel / no pseudo-tty (soexecute/terminalover SSH, which need an interactive-shell reader, are a deliberate later pass). This re-scopes the earlier "SSH lands as one unit" plan.transfernow routes--via sftp(and auto-selects it for >60 KB uploads, the gap the REST/file60 KB write cap leaves) through a new backend-agnosticFileBackendseam (AdapterFileBackendfor rest/native,SftpFileBackendfor sftp). Thessh-key(--ssh-key/CENTRS_SSH_KEY/ CDBssh-key=) andinsecuresettings land with the transport. Green against real CHR 7.23.1 (98 assertions,test/integration/transfer.test.ts): a key-auth sftp round-trip, the >60 KB upload, list/mkdir/remove, and example 17 (chunked REST read of an sftp-seeded file — the old fetch hack is gone). CHR finding: RouterOS's sftpls -ldoes not report a reliable byte size, so the sftp--verify sizetrusts the SFTP transfer guarantee rather than re-reading a size. New error codes:transport/host-key-mismatch,transport/insecure-trust,transport/auto-method,input/invalid-path. On-devicecopyhas no SFTP primitive and stays on rest/native. -
Unified TLS / SSH host-key trust across every transport. One opt-out —
--insecure(CENTRS_INSECURE, CDBinsecure=) — with verify-by-default and atransport/insecure-trustwarning when it is set. REST (fetch) and native-api (api-ssl) now both verify TLS by default (native-api previously accepted any cert silently); a self-signed cert fails withtransport/tls-certificatewhose remediation names--insecure. SSH host keys default toaccept-newtrust-on-first-use; a changed key fails withtransport/host-key-mismatch. Seedocs/CONSTITUTION.md, Transport trust. -
transferiscodedforrest-api/native-api.src/transfer.ts+src/cli/transfer.tsimplement the file verb —upload/download/list/remove/mkdir/copy(plus top-levelupload/downloadaliases) — over the RouterOS/filemenu, driven through the sharedProtocolAdapterexecute/listseam so REST and native share one path. Size/direction-aware method selection encodes the asymmetric/fileplumbing (writes capped at 60 KB via/file/set contents; reads scale via chunked/file/read), aprint-probe enforces validate-before-write (refuse-overwrite unless--force), a leading/in a remote path is normalized away, andsftp/scp/fetch/ftpreport a defined not-implemented / gated error. Unit-green via mockedfetch(test/unit/transfer.test.ts).test/integration/transfer.test.tsis green against a real CHR 7.23.1 (98 assertions, after the sftp commit below also seeds example 17) — confirming the/fileget/set/add/copy/removewire shapes over both REST and native — covering all examples except the three deferred for harness reasons (8–10 stdin/stdout/default-local), so the cells staycodedshy of the strict every-exampleCHR-passedbar. New error codes:usage/target-exists,transport/incomplete-transfer,transport/checksum-unavailable,input/local-file-not-found,settings/unsafe-protocol-blocked. -
discover / mndpisCHR-passed. A real layer-2 integration test (test/integration/discover.test.ts) boots a CHR with a secondsocket-connectNIC and a host bridge (test/integration/mndp-l2-bridge.ts) that lifts each frame's UDP/5678 payload into centrs's unmodifieddiscover()listener and injects MNDP refresh frames back over the link. It captures and decodes a genuine RouterOS MNDP announcement (CHR 7.23.1), cross-checksidentity/platform/board/version/macagainst REST, and--saves amacTarget/group=discoveredrecord. Thesocket-connectapproach was first proven against quickchr's ownexamples/mndp/guinea-pig run. The same bridge (frame-injection write-back) is the L2 harness mac-telnet will reuse. -
Integration CHR can now boot a chosen arch + extra packages.
startIntegrationChrreadsCENTRS_CHR_ARCH(arm64/x86) andCENTRS_CHR_PACKAGES(comma/space list) and threads them through quickchr 0.4.2'sStartOptions;verify-extended.yamlgains apackagesinput that installs them into the CHR (e.g.container) for a fuller-RouterOS sweep. The arm64 path is wired on the centrs side but no arm64 job is added yet: quickchr 0.4.2 has an arm64 REST-POST bug (returns the prior GET's body) that breaks the execute path every integration test uses, so an arm64 job would be known-red until that upstream fix lands.
-
package.jsondeclares itsrepository(andhomepage/bugs). Without it,npm publish --provenancefailed validation (E422: emptyrepository.urlvs the GitHub-Actions provenance source). The package now points atgithub.com/tikoci/centrs, matching the signed provenance. -
Flaky must-pass CHR cells no longer red the release gate. The first
v0.1.0release sweep was blocked by transient long-term failures (native-apiECONNREFUSED, mac-telnet L2 prime-latency) — the same cells pass locally and the reds hop channels run-to-run, i.e. CI timing flake, not regressions. Two fixes: (1) the native-api integration test nowwaitForBoots and retries the cold connect past the boot-readiness race (the api service can lag the REST endpoint), via awithBootReadyRetryharness helper that retries only transient connect errors —auth-failedstill propagates, so assertions are unweakened; (2) the qa.yaml CHR sweep runs integration per-file with one retry — a real regression fails both attempts and still gates, but a single transient flake is absorbed, and per-file isolation avoids the resource contention that fast-fails a later file. -
windows-latestunit tier is green. It was perpetually red on two causes, now both addressed without losing coverage: (1) Windows has noSO_REUSEPORT, so the MNDP/btest UDP loopback binds (reusePort: true) threwENOTSUP— a capability probe (test/unit/udp-loopback.ts) now skips just those UDP tests on such a runner (TCP + CHR integration keep the coverage), gating only on the known-unsupported bind codes so real regressions still surface; (2) thessh/sftpargv tests asserted/dev/nullwhere the src correctly emitsNULon Windows, andtransfer.basename()split on/only so an omitted upload remote defaulted to the fullC:\…path — it now splits on\too. -
btestbidirectional TCP server tx is now accounted. Adirection=bothTCP session reportedtotalTxBytes=0/txAvgBps=0on the server side even though it transmitted the client's receive half (hundreds of MB): the server's bulk-TX loop flushed only its rx into each interval, never its own tx. The server now records both halves, so abothsession'sdata.sessions[]and the CSV/text renders carry a non-zero tx rate. Loopback-grounded (test/unit/btest-{session,command}.test.ts); UDPbothand single-direction TCP were already correct. -
btesttotals now flush the final partial interval. The per-interval accounting loops recorded totals on a tick cadence and exited without folding in bytes that arrived after the last tick (or before the first, on a slow host), so a short run under-reported the final fraction of a second of each direction.driveSessionnow folds the remaining counter bytes into the totals on stop, keepingdata.reports[]lossless against them. -
discoverdefault listen window is now15s(was60sin code). Lowering the window relied on the up-front refresh broadcast (sent immediately, then every 5s) so responders reply within a round-trip; the per-command docs already documented15s, butDISCOVER_DEFAULT_TIMEOUT_MS, the CLI help, and the MCPcentrs_discovertool description still said60s. They now agree.
release.yamlfails closed whenNPM_TOKENis missing. A guard step in the publish job errors with an actionable message ifsecrets.NPM_TOKENdoes not reach the runner (whether a repo secret or a tikoci org secret with centrs in its repository access) — so a dry-run dispatch proves it before any real publish, instead of failing opaquely insidenpm publish. The value stays masked; only its presence is logged.- Workflow restructure. Replaced
docs/specs/,docs/WORKFLOW.md,docs/ROADMAP.md,docs/ARCHITECTURE.md, andwork/with three durable surfaces:docs/CONSTITUTION.md— the load-bearing rules (validation as product, result envelope, error model + URL scheme, settings precedence, identity, protocol selection, done = CHR-passed).docs/MATRIX.md— command×protocol grid; the only status surface.commands/<name>/README.mdandcommands/<name>/examples.md— per-command design and executable example list that gates "done."
- Done definition codified in
.github/instructions/done-definition.instructions.md: a feature is done when its examples are green on real CHR viabun run test:integration. Disabling validation to make a test pass is forbidden; validation is the product.
docs/specs/S001–S008,docs/WORKFLOW.md,docs/ROADMAP.md,docs/ARCHITECTURE.md, all ofwork/, and thedocs-specs-lifecycle/work-directory/integration-testinginstruction files. Their durable content folded into the constitution and per-command files; the rest was scaffolding for a workflow that did not pay off.