Commit Graph

3516 Commits

Author SHA1 Message Date
Joseph Doherty 336e79ea9a fix(driver.sql): compare-and-swap the health field so a concurrent transition cannot be lost (Driver.Sql-010) 2026-08-10 08:05:23 -04:00
Joseph Doherty cb7096ddff docs(reviews): point the -003 Won't Fix at the commit that carries it 2026-08-10 08:05:21 -04:00
Joseph Doherty 3427370831 docs(reviews): close Driver.Sql-009 + Driver.Sql.Browser-001/002/003
-003 is a documented Won't Fix: the mitigating control is the ConfigEditor page
policy on the only two surfaces that reach a browse session (/raw and /uns),
there is no HTTP endpoint for browse, and PageAuthorizationGuardTests pins both
pages to that exact policy. Recorded in docs/security.md under Control-Plane
Authorization and pointed at from SqlDriverBrowser's class summary.
2026-08-10 08:05:12 -04:00
Joseph Doherty 1fe0017600 docs(code-review): close Driver.Sql-002 + Driver.Sql.Dialect-001/002/003
Dialect header 3 -> 0 open. The Driver.Sql header count is left alone for the
orchestrator to reconcile — sibling agents are editing that file in parallel.
2026-08-10 08:04:40 -04:00
Joseph Doherty 2637be8212 docs(code-review): close Driver.Calculation-005 + Driver.Calculation.Contracts-001
Both modules go to 0 open findings. -005 records the concurrency guarantee the fix rests
on (at most one evaluation at a time: single pump + _evalGate, lock order _evalGate ->
_lock), the RED evidence, why the dependency snapshot is taken at trigger time, why the
timer path was left on its own scheduler, and the unbounded-queue trade. -001 records that
the equivalence is exact (Inspect's pre-checks already establish TryReadTagConfig's) and
that its new pin was verified against the pre-refactor code.
2026-08-10 08:04:01 -04:00
Joseph Doherty eddf5f57a5 fix(mqtt-browser): converge the plain-mode label guard, make the node cap atomic, total the sibling sort
Driver.Mqtt.Browser-002/-003/-004/-005.

-002: ObservePlainTopic's pre-pass judged segment LENGTH only, so a plain topic
segment carrying a newline or an ANSI escape became the picker's displayed label —
a rule ObserveSparkplugBirth already applied via IsUsableLabel. The control-character
half is now single-sourced (HasControlCharacter) and the plain marker text converges
with the Sparkplug one. An EMPTY topic level stays accepted on purpose: it is legal
MQTT and its terminal leaf is a real committable address.

-003: GetOrCreate read _nodeCount and incremented it later, so N observers could each
see cap-1 and all insert. The slot is now reserved with one Interlocked.Increment and
given back on both non-consuming paths.

-004: Project ordered a level by Segment alone, but the level is keyed by Path
precisely because two Sparkplug siblings CAN share a Segment — leaving the tie to
ConcurrentDictionary enumeration order, which .NET's randomised string hashing makes
differ per process. .ThenBy(Path) makes the sort total.

-005: no behaviour change — DefaultNodeCap's doc now states the real per-session
ceiling and records that the aggregate across sessions is bounded by the reaper's
idle TTL, not by any session cap.
2026-08-10 08:03:47 -04:00
Joseph Doherty 401d670639 docs(sql-dialect): record the browse consequence of Oracle's ALL_USERS schema list (Driver.Sql.Dialect-003)
ListSchemasSql has two consumers and the remarks only justified the divergence
from the catalog gate's viewpoint. SqlBrowseSession.RootAsync renders every row
as an expandable schema, so an Oracle browse offers every database user. The
narrower ALL_TABLES/ALL_VIEWS query is named, along with why it is not taken
here: it narrows the gate too, and that is a poll-path behaviour change only a
real Oracle server can gate.
2026-08-10 08:03:13 -04:00
Joseph Doherty 73230a42c5 perf(calc-contracts): single-parse Inspect (Driver.Calculation.Contracts-001)
Inspect parsed the TagConfig blob once itself (root kind + the present-but-discarded
changeTriggered/timerIntervalMs checks) and then called TryReadTagConfig(reference, out _),
which parsed the same string from scratch again for the scriptId rule. The field-by-field
read is now TryReadRoot(JsonElement), which both entry points share, so Inspect consults
the one authority over the root it already has. The public string-based TryReadTagConfig
signature other modules consume is unchanged.

Pure refactor: added CalculationTagDefinitionFactoryInspectTests (17 cases pinning the
exact wording, the rejection-vs-advisory split and their order, and the
never-stricter-than-the-runtime-read invariant). They pass unchanged against the
pre-refactor code — verified by stashing the production change.
2026-08-10 08:02:51 -04:00
Joseph Doherty a0a49d3395 fix(driver.sql): advance LastSuccessfulRead on evidence the database answered (Driver.Sql-006) 2026-08-10 08:02:49 -04:00
Joseph Doherty 886ce1b87c fix(sql): chunk the key-value IN list at the dialect's bind ceiling (Driver.Sql-002, Driver.Sql.Dialect-002)
ISqlDialect gains MaxBindParameters — the per-command ceiling the seam was
missing while modelling everything else that differs per backend: 2098 usable
of SQL Server's 2100 (sp_executesql spends two), 1000 for Oracle's IN-list
limit (ORA-01795, tighter than its 64k bind count), 65535 for the Postgres
Bind message.

SqlGroupPlanner.AddKeyValuePlans now emits several plans for one group when
its distinct keys exceed that ceiling, instead of one statement the provider
refuses after the connection opened — which left every tag in an EAV group
publishing BadCommunicationError on every poll, permanently, and reported the
database endpoint as down. Chunks share the group key; members follow their
own key so a shared keyValue never splits. SqlPollReader is unchanged: it runs
one command per plan and slices each plan's own Members back, proven by a
fixture read at a ceiling of 1.
2026-08-10 08:02:44 -04:00
Joseph Doherty 235e06a875 fix(calc): evaluate off the mux callback thread and out from under the state lock (Driver.Calculation-005)
OnDependencyValue ran every change-triggered dependent's script INLINE, under _lock,
via TimedScriptEvaluator.RunAsync(...).GetAwaiter().GetResult() — on the Akka dispatcher
thread of Runtime's DependencyConsumerMuxAdapter, for up to RunTimeout (2 s default) per
arrival, with the state lock held for the whole run.

The callback now records the value, PREPARES the affected tags under the lock (definition
+ warm-up gate + dependency snapshot taken at trigger time) and queues them for a single
serialized evaluation pump. _lock is a short-held state lock again; a new _evalGate
serializes the pump against the timer ticks and is taken only by evaluation workers, so
no host thread can wait on a script. Lock order is always _evalGate -> _lock.

Semantics preserved: per-tag ordering (one FIFO pump), the warm-up gate BEFORE the
evaluator runs, dedup/Bad-transition/recovery publishes, and — because the dependency
snapshot is taken when the trigger fires rather than when the pump catches up — a burst
of arrivals still evaluates against the values that triggered it.
2026-08-10 08:02:44 -04:00
Joseph Doherty 8d4ca5489a fix(tagconfig): a numerically-spelled enum names no member — reject it fleet-wide (Driver.Sql.Contracts-002)
TagConfigJson.ClassifyEnumText classified via Enum.TryParse(text, ignoreCase: true),
which accepts a numeric STRING for any enum: "1" returns the member at that ordinal
and an out-of-range "99" parses just as happily. Every driver contract's strict enum
read inherits that, so a Sql tag authored "type":"99" was ACCEPTED, carried
(DriverDataType)99, and SqlPollReader.TryCoerce fell through its switch to the String
arm — a stringified cell published Good under a type nothing can map.

Refuse a digit- or sign-leading spelling before the parse (judged on the TRIMMED text,
because Enum.TryParse trims), and require Enum.IsDefined — which covers what the
leading-character check cannot, since TryParse also ORs a comma-separated list for a
non-[Flags] enum. Mirrors RawBrowseCommitMapper.ParseNativeType and
MTConnectTagDefinitionFactory.TryParseDataType, which already guard their own parses.

Three AdminUI restatements of the same rule now ask ClassifyEnumText instead of
Enum.TryParse (TagConfigJson.GetEnum, SqlTagConfigModel.ReadEnumStrict,
MqttTagConfigModel.DescribeInvalidEnum/GetEnumNullable) — leaving them would have opened
the #549 direction the moment the runtime tightened: a blob the modal accepts and the
driver skips.

RED first: 10 failing in Core.Abstractions.Tests, 4 in Driver.Sql.Tests, and 10 of the
11 new fleet-wide parity cases (re-confirmed by reverting the Core fix).
2026-08-10 08:02:18 -04:00
Joseph Doherty 2d7dffcc42 fix(mtconnect): volatile _options publication, IConvertible write guard, idempotent client dispose
Driver.MTConnect-005/-006/-007, each TDD'd red-first.

- -005: _options was the one field at the StartCoreAsync commit point published by a
  plain write and read lock-free from ReadAsync/WriteAsync/DiscoverAsync/HostName/health.
  Now Volatile.Write/Volatile.Read like _session/_binding/_announcedInstanceId, pinned by
  a source scan (MTConnectOptionsPublicationTests) that fails on any unfenced touch.
- -006: TryFormatValue guarded null and Array, but Convert.ToString renders ANY
  non-IConvertible value as its type name under a Good status. Guard is now the property
  the remark relies on; the accepted set (string/bool/numeric/DateTime) is unchanged.
- -007: MTConnectAgentClient.Dispose was a check-then-set on a non-volatile bool, so two
  racing disposals both passed and the loser cancelled an already-disposed CTS. Now an
  Interlocked.Exchange claim over an int, with the request seams reading it volatile.
2026-08-10 08:02:15 -04:00
Joseph Doherty dd1cd984cd merge: MQTT driver+transport review findings resolved (Driver.Mqtt-006/-007, Driver.Mqtt.Transport-002) 2026-08-10 08:01:34 -04:00
Joseph Doherty cd4b29a368 fix(sql-browser): close two races around the registry reaper (Driver.Sql.Browser-002)
(1) DisposeAsync disposed _gate unconditionally. SemaphoreSlim.Dispose does not
complete pending async waiters, and the in-flight call's Release() then threw
ObjectDisposedException from inside the semaphore (swallowed by the finally), so
a caller parked in WaitAsync was never released and its call never returned. The
gate is now left alive - the pattern SqlPollReader documents - so the waiter is
admitted and meets the session's own guarded ThrowIf(_disposed, this).

(2) LastUsedUtc advanced only on completion, so a slow catalog call (the AdminUI
allows 20 s) was invisible to the TTL reaper for its whole duration and a session
at the edge of its TTL could be disposed mid-call. It is now stamped on entry
too.
2026-08-10 08:01:30 -04:00
Joseph Doherty ee1f1b28e8 fix(driver.sql): a post-open query failure is not a reachability verdict (Driver.Sql-005) 2026-08-10 08:00:51 -04:00
Joseph Doherty a8cfc886a3 fix(sql-browser): rank the row-selector candidate so a date-only column is a last resort (Driver.Sql.Browser-001)
PreferredTimestampColumn nominated the first column mapping to DriverDataType
.DateTime, and that set includes date-only types (SQL Server / PostgreSQL
'date'). A table whose first date/time column is a 'date' committed
rowSelector.topByTimestamp = <date>, making the wide-row read ORDER BY <date>
DESC LIMIT 1 - which ties for every row written that day, so 'the newest row'
became storage order among the ties.

Time-bearing candidates now outrank date-only ones; a date-only column is still
stated when it is all the table has, so such a table stays bindable. Oracle's
DATE carries a time of day and is exempt - pinned by a control dialect that
differs from the SQLite one only in the Provider it reports.
2026-08-10 07:59:35 -04:00
Joseph Doherty 2545bd7f7d docs(code-reviews): close Driver.Mqtt-006/-007 and Driver.Mqtt.Transport-002
Records the disposition and rationale for each: the primary-host gate is no
longer disposed (matching MqttConnection's documented rule), a reconnect that
resolves zero filters for live topics now throws like the zero-granted case it
is indistinguishable from, and a password configured without a username is
warned about rather than silently dropped. Both module headers drop to 0 open.
2026-08-10 07:59:34 -04:00
Joseph Doherty 90f3016a3e fix(driver.sql): defer HandlePollError on the exception ReadAsync classified, not on its type (Driver.Sql-004) 2026-08-10 07:59:09 -04:00
Joseph Doherty 3f85ba985e fix(mqtt): stop disposing the primary-host gate a live reconcile still holds
Driver.Mqtt-006 (Low, concurrency). DisposeAsync disposed _gate while StopAsync
cancels the reconcile loop WITHOUT awaiting it, so a ReconcileAsync already in
flight could be inside _gate.WaitAsync, or on its way to the finally-release,
when the semaphore went away. The release then threw ObjectDisposedException
naming SemaphoreSlim, logged as a reconcile failure — telling an operator
nothing about what happened and hiding whatever the reconcile was really
failing on.

Left undisposed, matching MqttConnection's documented rule for its own lifecycle
primitives and its stated rationale: a late callback racing teardown should be a
benign no-op rather than an ObjectDisposedException naming the wrong type. Two
lifecycle types in one module disagreeing about this is what gets copied into
the third. Nothing is leaked — the gate holds no timer and no registration — and
ClaimAsync still re-checks _disposed under it, so no claim can be created after
dispose either way.

Test is deterministic rather than timing-dependent: the gate is taken before the
connect starts, so a MiniBroker in blackhole mode (TCP accepted, CONNECT never
answered) accepting the socket proves the claim is inside the gate when dispose
runs. RED: log.Captured contained "ObjectDisposedException".
2026-08-10 07:58:38 -04:00
Joseph Doherty 2298485bda fix(mqtt): a reconnect that re-establishes nothing tears the session down
Driver.Mqtt-007 (Low, error handling). OnReconnectedAsync throws when a
re-subscribe is granted zero filters — MqttConnection's contract is that a
throwing Reconnected subscriber tears the fresh session down and retries under
backoff, because a connected-but-deaf driver is the one outcome this path must
never serve. But when the live subscribed topics resolved to zero filters in the
first place it logged a warning and returned, reaching the same "nothing was
re-established" state by a different route and giving it the opposite answer.

Throw there too, and say so in the remarks so the type states ONE rule. It costs
no other tag — every live topic is in the set that resolved to nothing, so there
is nothing left flowing to protect, which is the whole reason the partial-
rejection case is exempt — and it is not permanent: the retry runs against
whatever Register has published by then.

Test reaches the state the only way production can: Register prunes
_subscribedTopics of the topics its new table no longer carries, but a SUBSCRIBE
already in flight records its own topic AFTER that prune ran, so a deploy landing
inside the round-trip leaves a live topic no authored tag resolves to. Paired
with the control that a reconnect with no live subscriptions at all stays a
no-op — throwing there would tear down the session of every driver whose tags
have not been subscribed yet.
2026-08-10 07:58:29 -04:00
Joseph Doherty 5d4dd17d7f fix(mqtt-transport): warn when a configured password is dropped for want of a username
Driver.Mqtt.Transport-002 (Low, correctness). Credentials were attached only
when Username was non-empty, so a config carrying a Password and no Username
connected anonymously with the secret silently discarded — the deployment reads
as authenticated and is not, which is the direction an operator cannot see from
the config.

Warned rather than sent. MQTT 3.1.1 may not carry a password without a
username, so the password-only CONNECT is not available on every protocol
version this factory serves, and synthesizing one under V500 only would change
the wire behaviour of deployments that connect happily today. The configured
values stay authoritative; the drop just stops being silent. The secret is
never formatted into the message.

Tests: the warning fires and does NOT contain the password, plus the falsifying
control that an ordinary anonymous config (neither credential set) stays silent
— without it the fix could warn on every connect and still pass.
2026-08-10 07:58:19 -04:00
Joseph Doherty 74594e6083 fix(sql-dialect): enforce Postgres/Oracle identifier ceilings in UTF-8 bytes (Driver.Sql.Dialect-001)
NAMEDATALEN-1 (63) and Oracle 12.2's 128 are byte limits in the database
encoding, not character counts, so a 32-character Cyrillic name passed
ident.Length and was then silently truncated (Postgres) or rejected by the
server (Oracle). Both dialects now measure Encoding.UTF8.GetByteCount and say
'bytes' in the rejection. SqlServerDialect deliberately keeps ident.Length —
sysname is nvarchar(128), a real character limit — pinned by a control test.
2026-08-10 07:58:04 -04:00
Joseph Doherty 073930e023 fix(sql): withhold the JSON parser's message from the probe's red result (Driver.Sql-009)
SqlDriverProbe surfaced 'Config JSON is invalid: ' + ex.Message, where
SqlDriverBrowser.ParseDocument deliberately withholds the identical message
because the malformed text may itself carry a connection string. The two read
the same blob shape - the AdminUI Sql picker composes a document with a
top-level connectionString literal - so a malformed paste reaches both. The
probe now reports the failure class only, matching the browser.
2026-08-10 07:56:58 -04:00
Joseph Doherty 7a367b4d86 docs(deferment): Driver.MTConnect-003 filed as #560 and closed same day (merge 5226f7fd) 2026-08-10 07:38:58 -04:00
Joseph Doherty 46cb6e0085 docs(reviews): regenerate README — Driver.MTConnect open findings 5 -> 4 (#560) 2026-08-10 07:37:25 -04:00
Joseph Doherty 5226f7fd41 merge: browse-committed MTConnect tags carry the browsed inferred type (Gitea #560 closed; Driver.MTConnect-003 Resolved) 2026-08-10 07:37:23 -04:00
Joseph Doherty 1c67d019e5 docs(mtconnect): describe the fixed browse-commit coercion type (#560)
The comments and docs that described Driver.MTConnect-003 as live at HEAD would
have been wrong the moment 64be41d8 landed:

- code-reviews/Driver.MTConnect/findings.md — Driver.MTConnect-003 Resolved,
  with the fixing commit; module open-finding count 5 -> 4.
- code-reviews/Driver.MTConnect.Contracts/findings.md — the one clause inside
  Contracts-001's prose that asserted 003 was Open.
- docs/drivers/MTConnect.md — the coercion-precedence warning now states that a
  browse-committed tag resolves at step 1 CARRYING the step-3 answer, and that
  step 3 remains the rescue for a hand-authored / CSV / API blob with no type.
- MTConnectDriver.BuildBinding, MTConnectDataTypeInference and
  MTConnectDriverOptions.RawTags remarks — same correction, each keeping a short
  historical note of what the defect was so a reader meeting the old blobs still
  understands them.

Comment-only in src; the driver suite (538) and AdminUI suite (1474) re-run green.
2026-08-10 07:36:45 -04:00
Joseph Doherty 64be41d80c fix(adminui): forward the browsed MTConnect leaf's inferred type into the committed TagConfig (#560)
RawBrowseCommitMapper.MapLeaf typed a browse-committed raw tag's OPC UA node
from the leaf's probe-inferred DriverDataType, but its BuildTagConfig MTConnect
branch emitted `new MTConnectTagConfigModel { FullName = address }` — and that
model's DataType defaults to String with ToJson() unconditionally writing
`dataType`. A non-blank authored type wins the driver's coercion precedence, so
the Agent-probe inference step was never consulted and every browsed SAMPLE
published text into a Double/Int64 node. Silent: the blob is perfectly valid, so
neither the authoring gate nor the deploy gate had anything to say.

Same defect class as #536, which is why the AbCip / TwinCAT / FOCAS branches
forward the browsed native type via NativeType(addressFields, …). MapLeaf now
passes driverDataType into BuildTagConfig, whose MTConnect branch uses it as the
NativeType fallback — a stated native `dataType` address field still wins, so
the branch reads like its siblings; the difference is only that MTConnect's
fallback is the leaf's own inference rather than a fixed model default. Its
coercion type IS a DriverDataType, the very enum the leaf carries, so nothing is
re-derived. NativeType's numeric-string and unparseable-name guards are shared
via the extracted ParseNativeType, so an unusable name still keeps String.

Tests assert the PAIR (row OPC UA type and blob coercion type agree with the
inference) — the blob alone would pass against a mapper that stopped typing the
row — plus the parity control that a leaf inferred (or reported) as String still
commits `"dataType":"String"`, so the forward cannot be implemented as a
deletion. Observed red first: expected "Float64" but was "String".
2026-08-10 07:35:12 -04:00
Joseph Doherty fda9fd2bf0 docs(reviews): cross-reference Gitea #560 in Driver.MTConnect-003 2026-08-10 07:22:28 -04:00
Joseph Doherty 94b8633e68 docs(deferment): seventh round — review-coverage gap closed (14 modules, 61 findings, 26 fixed); §0.1 row struck
v2-ci / build (push) Successful in 4m32s
v2-ci / unit-tests (push) Failing after 16m43s
2026-08-10 07:09:13 -04:00
Joseph Doherty fd7d06bb12 docs(reviews): regenerate README for 14 new module reviews; fix MTConnect finding-heading level (#### invisible to the generator) 2026-08-10 07:03:02 -04:00
Joseph Doherty d68009106b merge: Mqtt runtime + Transport code review — deaf-reconnect subscription reset (High), teardown-before-reinit session leak (High), timeout normalization, dead CA-pin warning; 12 findings 2026-08-10 07:02:15 -04:00
Joseph Doherty 147b624437 merge: Calculation + OpcUaClient.Session code review — warm-up gate before evaluation (spurious script-throw warnings), evaluator logger wiring, probe JSON-leniency parity; 7 findings 2026-08-10 07:00:33 -04:00
Joseph Doherty c58e833aee merge: Mqtt Contracts+Browser code review — wildcard PUBLISH refusal (whole-driver outage path), Sparkplug segment shape rules, rebirth-scope wildcard gate; 12 findings 2026-08-10 07:00:22 -04:00
Joseph Doherty 978eeda753 merge: MTConnect family code review — sample-pump hot-loop backoff, probe deviceName path, stale-premise docs; 9 findings (High browse-commit String coercion recorded cross-module) 2026-08-10 07:00:09 -04:00
Joseph Doherty 9ccc0d5678 merge: Sql family code review — default-schema fallback (whole-driver outage path), rowSelector doc fix, dead SqlClient ref; 18 findings recorded across 4 modules 2026-08-10 06:59:53 -04:00
Joseph Doherty fefb240350 review(mqtt-transport): findings for Driver.Mqtt.Transport at b990adae (2 findings, 1 resolved in 74b85271, 1 open) 2026-08-10 06:59:38 -04:00
Joseph Doherty 416c471809 docs(review): add code review for Driver.OpcUaClient.Session
No defects found across all 10 checklist categories. NamespaceMap's
namespace-URI-stable NodeId encoding and v3 RawPath resolution table match
docs/v2/driver-specs.md §8 and are exercised by
OpcUaClientNamespaceTests.cs. Zero findings recorded.
2026-08-10 06:59:37 -04:00
Joseph Doherty 74b852716c fix(mqtt-transport): warn when the untrusted-cert hatch voids a pinned CA
Driver.Mqtt.Transport-001 (Medium, Security). allowUntrustedServerCertificate
and caCertificatePath are independent settings, and when both are set the hatch
wins SILENTLY: ConfigureTls installs the accept-everything validator and
returns before the pin is consulted. The combination is not exotic — the hatch
is what an operator turns on while standing a broker up and the pin is what
they add afterwards — and the failure is silent in the direction that matters:
the config reads as pinned, and nothing validates anything.

Both knobs are explicit, so the flag stays authoritative (overriding it would
break a deployment that genuinely wants the hatch, and refusing to build would
fault a driver over a config we have been accepting). Saying so once, at the
seam that resolves the conflict, is the proportionate answer. No behaviour
change; the factory's optional ILogger already exists for this class of
deferred diagnostic and never formats credentials.
2026-08-10 06:59:34 -04:00
Joseph Doherty 33f7472002 docs(review): add code review for Driver.Calculation.Contracts
No functional defects found. One Low/Open record-only finding: Inspect()
double-parses its TagConfig blob (once directly, once via TryReadTagConfig)
by deliberate design of the module's single-authority split — not pursued
as a fix since it is deploy-time-only (not a hot path) and widening
TryReadTagConfig's signature to avoid it touches consumers outside this
module (Driver.Calculation, AdminUI).
2026-08-10 06:59:33 -04:00
Joseph Doherty b4a03b4baa fix(calculation): probe JSON options stricter than the real spawn/reinit path (review)
CalculationDriverProbe.Opts (Test-Connect) omitted AllowTrailingCommas and
ReadCommentHandling that CalculationDriverFactoryExtensions.JsonOptions
(spawn) and CalculationDriver.ReinitJsonOptions (in-place ApplyDelta) both
set, so a config blob the real deploy path tolerates could fail Test-Connect
as "invalid JSON" even though deploying it would succeed. Aligned the three
option sets. Also adds CalculationDriverProbeTests.cs, which previously did
not exist at all for this driver type's Test-Connect surface.

Part of code-reviews/Driver.Calculation/findings.md (-003, -004).
2026-08-10 06:59:29 -04:00
Joseph Doherty 95cdb6c578 review(mqtt): findings for Driver.Mqtt at b990adae (10 findings, 8 resolved in 2b4b3fcb, 2 open) 2026-08-10 06:59:26 -04:00
Joseph Doherty a836e0d983 fix(calculation): dead logger cast + warm-up evaluated before change-gate (review)
Code review of Driver.Calculation found two real defects, both TDD-fixed:

- CalculationDriver's ctor tried to reuse its ILogger<CalculationDriver> as the
  evaluator's ILogger<CalculationEvaluator> via an `is` check that can never
  succeed (unrelated closed generics), so CalculationEvaluator always ran with
  a NullLogger regardless of what the driver was constructed with. Fixed by
  having CalculationEvaluator accept the non-generic ILogger instead.

- EvaluateTag called the evaluator BEFORE checking whether every declared
  dependency had arrived, so a multi-dependency change-triggered calc tag
  evaluated its script against a partial snapshot on every warm-up tick.
  VirtualTagContext.GetTag returns a Bad snapshot (.Value == null) for a
  missing dep, so the idiomatic `(double)ctx.GetTag("x").Value` cast throws
  NullReferenceException — caught and logged as a spurious "script threw"
  script-log Warning on ordinary deploy/restart warm-up, for the standard
  multi-input calc-tag shape. Fixed by gating evaluation itself on
  "all deps arrived", not just the publish.

See code-reviews/Driver.Calculation/findings.md for full detail (5 findings:
001-004 Resolved, 005 Open/cross-module).
2026-08-10 06:59:23 -04:00
Joseph Doherty 2b4b3fcbbf fix(mqtt): session-rebind subscriptions, re-init teardown, timeout clamp
Findings from the per-module review of Driver.Mqtt at b990adae
(code-reviews/Driver.Mqtt/findings.md records all ten; these are the fixed
ones).

Driver.Mqtt-001 (High) — MqttSubscriptionManager._subscribedTopics is a claim
about the broker session it established filters on, and AttachTo never cleared
it, but the manager OUTLIVES a session rebuild (AdoptOptions keeps it whenever
the ingest identity is unchanged). After a driver-host reconnect
(ShutdownAsync + InitializeAsync, the #527 watchdog's retry) or a
ReinitializeAsync that only changed the endpoint/credentials, the host's
re-subscribe resolved an empty pending set, sent no SUBSCRIBE at all, and left
the driver Connected/Healthy on a session carrying ZERO subscriptions — no
error, no Bad status, values frozen until the new session happened to drop.
AttachTo now forgets them, mirroring SparkplugIngestor.AttachTo's reset.

Driver.Mqtt-002 (High) — InitializeCoreAsync published _connection and
_primaryHost over whatever the driver already held, so a second initialize
stranded a connected, self-supervising broker session (plus a second Sparkplug
primary-host claim) that no later dispose could reach. Reachable without any
teardown of ours via DriverInstanceActor.AdoptConfigDuringInit. Now tears down
first, under the gate both callers already hold.

Driver.Mqtt-003 (Medium) — connectTimeoutSeconds is [Range]-annotated but
unvalidated on the runtime path; 0 makes every deadline fire immediately and a
negative value throws ArgumentOutOfRangeException out of ShutdownAsync and
DisposeAsync. Normalized once, at the seam every options instance passes.

Also: ShutdownAsync keeps the live last-message instant (-004), three usings
left behind by #512 removed (-005), stale remarks corrected (-009, -010), and
docs/drivers/Mqtt.md now distinguishes the two kinds of reconnect.

Tests 708 -> 713. All three regression tests were confirmed to fail against
the un-fixed code, with two controls that pass in either state.
2026-08-10 06:59:18 -04:00
Joseph Doherty 7eba588305 docs(code-reviews): add Driver.MTConnect + Driver.MTConnect.Contracts findings (first review, b990adae)
Driver.MTConnect: 8 findings — 1 High (Driver.MTConnect-003, browse-committed tags
are typed String and defeat the probe inference; cause is in AdminUI's
RawBrowseCommitMapper, so recorded Open with a recommendation), 3 Medium
(001/002 fixed in 6c63bf8f, 004 the missing write.enabled authoring surface,
cross-module Open), 4 Low. 5 remain Open, all cross-module or record-only.

Driver.MTConnect.Contracts: 1 finding — a Low documentation defect whose false
premise concealed Driver.MTConnect-003; fixed in 6c63bf8f. No behavioural issues:
the accept-gate-first Inspect contract, the FirstNonBlank alias precedence and the
digits-only enum pre-check were all verified as deliberate and correct at HEAD.

README.md deliberately not regenerated (out of scope for this review).
2026-08-10 06:59:11 -04:00
Joseph Doherty ce5c6d093c review(Driver.Sql): stamp the fixing commit sha into the three Resolved findings 2026-08-10 06:59:05 -04:00
Joseph Doherty 256d943f1e review(Driver.Sql.Dialect): 3 Low findings, all record-only
001 the Postgres (63) and Oracle (128) identifier ceilings are BYTE limits but are
enforced as UTF-16 char counts, so a non-ASCII name passes the check and is still
truncated server-side — the exact silent truncation the check documents itself as
preventing;
002 the seam carries no max-bind-parameter member, which is why the planner cannot
chunk a key-value IN list (Driver.Sql-002);
003 Oracle's ListSchemasSql reads ALL_USERS, justified only from the catalog gate's
point of view — the browse tree shares the query and renders every database user as
an expandable schema.

Quoting, marker/bind-name split, BindByName and the type maps are all sound.
2026-08-10 06:58:41 -04:00
Joseph Doherty 368d8e9581 review(Driver.Sql.Contracts): 2 Low findings, both record-only
001 keyValue is read string-only while whereValue accepts any scalar, and Inspect
then reports a numerically-authored key as ABSENT — an authoring surface
disagreeing with the runtime about why (the #549 class);
002 cross-module: TagConfigJson's strict enum read rests on Enum.TryParse, which
accepts a numeric string, so "type":"99" is Valid and an undefined
DriverDataType reaches the address space. Fix belongs in Core.Abstractions.

The accept-gate-first Inspect and the strict enum reads are otherwise exactly the
#549-#559 arc's rules, implemented structurally.
2026-08-10 06:58:29 -04:00
Joseph Doherty 03eadae677 review(Driver.Sql.Browser): 3 Low findings, all record-only
001 the #552 topByTimestamp default admits a date-only column, so the committed
wide-row read orders by a column that ties for every row written that day;
002 the session's gate is disposed under a racing call and LastUsedUtc only
advances on completion, so a long expand is invisible to the TTL reaper;
003 the ad-hoc pasted connection string is an arbitrary outbound-connect primitive
— correctly behind the /raw ConfigEditor policy, but undocumented as such.

No code changes: the NodeId codec, the bound-parameter catalog path and the
connection-ownership handoff are all sound. 79/79 tests unchanged.
2026-08-10 06:58:20 -04:00