tom_core_flutter 1.2.1
tom_core_flutter: ^1.2.1 copied to clipboard
Flutter integration layer for the TOM framework — re-exports tom_core_kernel and adds Flutter widgets and client utilities.
Unreleased #
Added #
- Bulk preference write:
TomUserPreferencesClient.setAllandwritePreferences.setAll(values)encodes a key → value map assetencodes one and sends it throughwritePreferences(POST /preferences, the kernel'sTomUserPreferenceBulkWriteRequest), which the server writes all or none and answers ordered by key;writePreferencesvalidates every key before sending.all,read,getandgetOrreturn the defaults the server answers under keys the user has not written,TomUserPreferenceDto.originsaying which.doc/preferences.md§ Reaching the Account Store; tests intest/preferences_test.dart. - Passkeys:
TomWebAuthn2FAClientMechanism. The second shipped client mechanism, registered under'webauthn'by the flow whereverTomWebAuthnPlatform.instancereports a platform authenticator. Its panels are a sentence and one button (Safari requires a user gesture), and they hand the server's options to the platform and submit the authenticator's response through the existingStringseam, encoded bytoSubmission(). On the webTomWebAuthnPlatformreachesnavigator.credentialsthroughdart:js_interopwith no plugin; elsewhere it isTomUnavailableWebAuthnPlatform, so the chooser omits passkeys and a passkey challenge reads as unrenderable. New text keys undertwo_factor.webauthn.*. TheonSubmitdartdoc now says the string is what the mechanism collected, not only what the user typed.
Changed #
-
TomClientConfigurationStore.writeis replaced byupdate(change), a read-modify-write the store performs as one step.save()merges inside it, so two saves that overlap — two holders in one isolate, two isolates, or two processes over one file — no longer lose the earlier one's change. The memory store runs the change with no await between read and write; the file store runs it under the kernel'stomWithFileLockAsyncguard on<path>.lock. Seed a store withstore.update((_) => {...}). -
A
null-valued config key is reported as such. An obligatoryTomPropertyoverapp.region:still fails withresources.property.not_configured, but the message now says "is configured without a value" instead of "not found in configuration resources", which sent the operator to a line already in the file. The check reads the provider's own map on the failure path only. -
Follows
tom_flutter_ui's rename of its last unprefixed public types (TomValidators,TomValidationError,TomSelectableItem,TomFileRef, theTom…Eventfamily, …). -
The API summaries moved from
doc/api_reference/todoc/api/, where every other package and the workspace guideline already kept them. The folder held only the markdown summaries and their index; the nameapi_referencenow means only the built PDF,doc/{package}_api_reference.pdf. -
TomBaseClientConfiguration.save()writes only what this holder changed. A declared key is written, or removed, when its override differs from the snapshot of the lastload()orsave(); every other key is carried forward from a fresh read of the store, and the holder then adopts those values, so holder and store agree. Two holders over one store location no longer erase each other's keys, and an external edit to an untouched key survives a save. A stored entry that equals the baseline is now carried forward instead of removed, but setting a value back to its baseline still removes the key. Two saves that genuinely overlap can still lose one change. -
Registrations use
TomBean.withType<X>(bean), following the kernel's move of the key into a type argument. Reflection artifacts regenerated.
Tests #
test/ai_citation_test.dartfails the suite when an artefact of the package cites a document in the AI-state mount. It runstom_ai/core/tool/check_doc_ai_citations.pyover the package throughtom_ai/core/tool/package_gate.dart; the rule and its one exemption (the runtime_ai/trail/directory) are stated in the checker alone.test/test_comment_hygiene_test.dartno longer carries its own copy of the rule.test/formatting_test.dartdelegates to the shared guard. The walk, the generated-file exclusion, the coverage floor and the choice of formatter moved totom_ai/core/tool/package_format_guard.dart, which every package undertom_ai/coreshares; the file keeps only this package's floor, landmarks and generated-output flag.
1.2.1 #
Fixed #
-
A platform refusal is now this module's failure (tcopen225). A browser refusing a write for an exhausted quota threw its own
DOMException;dart:iorefusing one for a full disk or a read-only support directory threw aFileSystemException. Both propagated out ofsetunchanged — so the one storage failure a correctly configured client actually meets was the one with no code to branch on, in a module whose exception type exists precisely so callers branch on codes.Both backends translate now, and both directions:
device_preferences.store.unreadablefor a read the platform refused,device_preferences.store.write_refusedfor a write.The code names which operation, not why. A full disk, a read-only directory and an exhausted quota are different events with different remedies, but
dart:ioreports them as one type distinguished by a per-platform OS error number and a browser by aDOMExceptionname the interop type does not promise across engines — so a cause classified here would be a guess wearing a code. The original travels onrootException.A failure this module already named passes through untouched: a conflict, an undecodable document, an owner mismatch, an uninitialised reflection.
Added #
-
The registry's one-route claim is held to the code (tcopen242). Removing
TomClient2FAAdaptorleft a sentence in three documents —registeris the only route into the registry, there is no annotation and no scan — with nothing checking it, which is the same shape of unchecked claim the annotation itself had been. Three assertions check it now: exactly one insertion site into the catalog, no reflection anywhere in the module's sources, and no entry for the module inbuild.yaml'sgenerate_for— the last because an annotation route needs generated mirrors to read a marker with, and the build configuration is the half a reader would never think to look at. -
A lapsed offer or challenge says so (tcopen241). Both wire types carry an
isExpiredand the flow read neither, so a user who left the set-up screen open scanned a QR code that would not be accepted, typed a confirmation, and got a refusal from the server — correct behaviour arriving as late as it possibly can. The flow now checks at the two stops where a deadline exists and renders a line above the panel:two_factor.error.enrolmentExpiredandtwo_factor.error.challengeExpired.A notice, not a refusal. The panel and its submit button stay, and whatever the user sends is judged by the server.
expiresAtis an absolute instant the server issued and there is no skew window on this path, so the client's verdict is only as good as this device's clock — a flow that hid the panel on its own verdict would lock a user whose clock runs fast out of a login the server would still complete.The chooser never reports a lapse, and a begun offer with no deadline reads as lapsed; both readings are the kernel's and are inherited rather than re-derived. A deployment's bundle needs the two new keys.
Changed #
-
TOTP is stated as the only mechanism the framework ships (tcopen240). SMS and WebAuthn are named in four places across the second-factor module — the argument for having a registry, every registration example, the
hintfield's illustration, the TOTP mechanism's preamble — and neither exists. The documents read as though they did, so a reader deciding whether the framework covered their SMS requirement had to grep to find out it does not.Nothing changed about what ships. The boundary is now stated once in each artefact, with the reason the illustrations exist — a pluggable design argued only against its one built-in proves nothing — and with where the boundary runs: the client half of an SMS factor is two fields, and the half that matters is a server adaptor that can deliver a message.
The claim is held by the suite in two places: the registry must hold exactly
totpafter the built-ins install, andlib/must declare exactly oneTom2FAClientMechanism— the second because a mechanism declared and left unregistered would still be part of the package's surface. -
What an application may show a 2FA panel is stated as its own rule (tcopen239). "A mechanism receives the decoded
clientDataand nothing else about the login" is about credential material — no principal, no tokens, no backend handle — and it had been carrying a second question it does not answer: what the application may show a panel. A device name is not credential material.The answer, now written down: named fields on the context, supplied by the controller, one per case that needs it.
accountLabelis the first and so far only one. An untypedapplicationDatabag was declined — one opaque map on this path is the server'sclientData, and a second with different provenance is how the two get confused — and so was routing presentation data through the server.The challenge context's having no application field is an absence of demand rather than a rule, which the documents now say instead of leaving the asymmetry unexplained.
Both decisions are pinned by shape: the suite reads each context's declared fields and holds them to an exact set, so adding a bag or giving the challenge context an
accountLabelfails a test rather than passing as an implementation detail. No API changed. -
A challenge for an unrenderable mechanism says which problem it is (tcopen238). It reached the chooser's
noRenderableMethodsline — accurate about the registry, wrong about the situation, since "this app cannot set any of these up" is written about enrolment and a user being challenged is not setting anything up. The challenge stop has its own key now,two_factor.error.unrenderableChallenge, under its own widget keytom-2fa-unrenderable-challenge, naming the thing that would fix it: a newer build.Two smaller wrongs in the same spot went with it. The attempt budget was printed beside the message — a countdown of tries the user has no way to spend — and is now rendered only beside a panel that can spend one; and the skip affordance's absence, which is enrolment-only by construction, is asserted rather than incidental.
The framework does not pretend to recover the state: the deployment is a client behind its server, and no affordance on this side supplies the missing panel. A controller hook so a deployment could offer its own exit is the right shape if one ever asks for it.
A deployment's bundle needs the new key.
-
The mechanism registry's registration window is documented (tcopen237).
Tom2FAClientRegistry.registeris a start-up call. Nothing bounds when it may be run, the registry is not sealed, and the flow resolves the mechanism on every build — so a registration made during a live flow lands on the next frame.What that costs was measured rather than assumed, and it is narrower than it looked: a replacement rendering a different panel widget takes the screen over and the typed input goes with it, while one rendering the same widget keeps it, because Flutter reuses the element. An application that wraps or subclasses the shipped panel — the likelier substitution — loses nothing.
Sealing the registry after the first flow build was declined: it would forbid a mechanism arriving from a plugin loaded after the login screen is up, to prevent a misuse no shipped path performs.
Added #
-
The TOTP challenge panel counts down (tcopen236). A code lives one step, and a user who typed slowly got a refusal that said only that the code was wrong — nothing separated "you typed it wrong" from "you typed it too late", and the attempt was spent either way. The period was already on the wire and the panel was discarding it.
It reads it now through
TomTotp2FAClientMechanism.periodOf, and renders a countdown seeded bysecondsRemainingIn— which needs no server timestamp, because TOTP steps are aligned to the Unix epoch and the boundary is the oneTomTotp.counterFordivides by. A second line appears when a rollover overtakes something already typed, and only then; any edit withdraws it.It warns and does not block.
TomTotp2FAServiceaccepts one step either side of its own, so a code typed just before a rollover is still good — a panel that refused to send it would enforce a policy stricter than the one that judges it, and cost the user the very attempt it was trying to save.Two resource keys are new:
two_factor.totp.challenge.expiresIn(carrying{seconds}) andtwo_factor.totp.challenge.codeChanged. A deployment's bundle needs both.
Changed #
-
One HTTP client for every native target, recorded as a decision (tcopen235). The native variant carried a note: "iOS/macOS could use a native CupertinoClient (cupertino_http) for better platform integration; for now all native targets share this IOClient". Honest, and the one shape a decision cannot be checked in — a "for now" is the absence of a choice, and nothing could fail if it stopped being true.
cupertino_http'sURLSession-backedCupertinoClientis declined: a dependency carrying a native plugin, and a second HTTP path no Linux or Windows host can exercise, for a benefit no consumer of this framework needs today. If an Apple target appears, the shape is a runtime branch ondefaultTargetPlatforminside the native variant — not a fourth compile-time variant, becausedart:ioexists on iOS and macOS and the conditional import cannot separate them. That is recorded where the branch would go.Three assertions hold it:
httpClient()answers anIOClientfor everyTargetPlatform, iOS and macOS included; the client completes a real request against a loopback server, so the shared path is exercised on the host running the suite; and neither the variant's source nor the pubspec may name a Cupertino client. -
The isolate name is documented as a label, and the neutral variant says
"neutral"(tcopen234).getIsolateName()is printed in every log line and nothing said what it could tell apart. It is a label, not an identifier: the native variant answers the isolate'sdebugName(or"main"when it reports none), the web variant the constant"web", the neutral variant now"neutral"rather than the abbreviation"ntrl", which expanded nowhere and matched neither sibling.The limitation's shape was measured rather than assumed. The root isolate's
debugNamereally is"main", so the?? "main"fallback coincides with it instead of colliding with it; andIsolate.spawndefaultsdebugNameto the entry point's function name, so isolates spawned from one entry point share a name while isolates from different entry points do not. An explicitdebugNamediscriminates, and the kernel'sTomWorkerPoolalready passes one per worker.No unique id is added — it would mean either names that change between runs or a second member on the
tom_basicscontract this package does not own. The suite had asserted onlyisNotEmpty; it now pins the contract that was chosen, so moving to a unique id later is a decision rather than a drift. -
The three platform variants share a base (tcopen233). Each declared the nine platform predicates byte for byte — about twenty-five lines in triplicate. They now extend
TomFlutterPlatformUtilsBase, a fourth file all three import unconditionally; it has to be a fourth file and a different name, because the conditional import requires the variants to share a class name.The extraction had been deferred on the ground that the variants are compile-time-selected and cannot all be exercised from one host. True, and an argument for the base: no host compiles more than one variant, so while each carried its own copy two of the three were untested by construction. The base has no conditional import and is deliberately concrete, so the suite now drives the shared predicates over every
TargetPlatformFlutter declares.getBrowserLocation()moved with them — native and neutral both answerednull, which the new "no member is written identically by two variants" guard found immediately. It answersnullon the base, whereTomFallbackPlatformUtilsanswers"", because a target that is not a browser has no location; the web variant overrides.The shared base holds ten members and the variants differ in four:
httpClient(),getIsolateName(),toString()and the web variant'sgetBrowserLocation(). The standing guard is a rule rather than a number — no variant may restate a body the base already gives it. -
clientPlatformUtilsanswers one instance per process (tcopen232). It constructed on every read, so a caller who read it twice held two objects — and on the native variant each construction re-copied the whole process environment into the kernel's static env-var map, on a call that reads like a property access. It caches now: the first read builds the variant, every later read answers the same object.A cache rather than a constant, because the target is only known at run time on the web branch and the constructor does real work on the native one. Caching the object does not freeze what it reports — every predicate reads
defaultTargetPlatform/kIsWebat call time.Identity now means something: a caller that reads the getter again instead of going through
TomPlatformUtils.currentholds what the process registered, so comparing platform-utils identities is no longer silently always false.resetClientPlatformUtils()discards the instance so the next read constructs again.@visibleForTesting, and there for one reason: what a constructor does can only be observed by a test that can get back to "nothing built yet" — the same shape and reason asTomConfigResourceProvider.clearAppConfigProvider.
Added #
-
The certificate allow-list covers both loopback families, and is an application's to change (tcopen231).
TomBadCertificatePolicyreplaces the native variant'sallowsBadCertificateForHost. The shipped list is0.0.0.0,localhost,127.0.0.1,::1and[::1];::1was missing, and because which familylocalhostresolves to is the resolver's choice, the same code, certificate and URL succeeded on one desktop and failed on the next — as a TLS error, which points the reader at the certificate rather than the address family.::1is the spelling that reaches the callback: it is handedUri.host, andUristrips the brackets an IPv6 literal carries in a URL.allowOnlyreplaces the list — widening it to a named development host, or narrowing it to nothing for a hardened build — andreset()restores the shipped policy. Replacement rather than addition, because narrowing is the direction that matters for a security boundary.Three things stand in for the property a
constlist had, that the policy in force was the policy in the source: the default is stillconstand auditable in one declaration,hostsanswers an unmodifiable list so the boundary cannot widen without passing through the validated call, and every replacement is logged at warn level naming the hosts. A blank entry is refused with anArgumentError, before anything takes effect. Matching is exact — no patterns and no wildcards — and the policy is read per handshake, so a narrowing reaches clients that already exist.The module's exported surface grew from one name to two.
TomBadCertificatePolicyis a file of its own rather than a member of the native variant: it has no platform dependency, so it exists on every target and exports unconditionally, where a policy inside the variant could only be reached through asrc/import the module's own documents call internal.TomFlutterPlatformUtilsremains withheld. -
Where properties stop is documented, and measured (tcopen230). The four typed subclasses are scalar; a structured config value belongs to the generic —
TomProperty<List<dynamic>>,TomProperty<Map<String, dynamic>>— and there is deliberately noTomListProperty/TomMapProperty.Two facts about holding one are now stated in the class dartdoc, the module readme, a section of
doc/resources.mdand the API summary. The element type has to bedynamic, because every configuration format arrives through a decoder and none produces a narrower one — and the narrow form fails selectively: a hand-written config map does yieldList<String>, soTomProperty<List<String>>passes in a test fixture and throws once the same values load from a file. And a collection is held by reference — the provider's own object, cached — so mutating what a property handed back edits the configuration every later reader sees. Copy at the call site, which is the samefromcall the element type already wanted.The reason no collection property ships is recorded with the shape to add if a consumer appears:
TomListProperty<E>alone, element-wise cast at construction, unmodifiable cached copy, and no map property until the rule relating a map property to a dotted scalar key over the same subtree is settled. -
Eager resolution is stated as a contract, and tested (tcopen229).
TomPropertyhas always taken its value at construction and never re-read it; the obligation that creates — populate the config providers first — lived in one dartdoc sentence with nothing behind it. It is now stated in the class dartdoc, the module readme,doc/resources.md(a section of its own) and the API summary, with the half that is enforced separated from the half that is not: a property built with no provider installed throws the kernel'sresources.config_provider.not_installedat the declaration, while a provider replaced afterwards is silent and leaves every property built before it holding what it resolved.Silent by design —
setAppConfigProviderannounces nothing, because configuration is captured by its consumers when they are built, so a revision counter would notify values already taken. A default follows the same rule: one taken because the key was absent is the resolution, not a placeholder.The documents now name the alternatives — replace configuration at a session boundary and rebuild its dependents; read the provider at the point of use for a value that must be live; use the client-configuration module for a setting a user changes — and the example ends by replacing the provider and printing the property built before beside the one built after.
-
The account store can be cleared (tcopen227).
TomUserPreferencesClient.clear()deletes every preference of the calling principal in one request, against the kernel contract's newDELETE /preferences. It is the entry that had been missing from the claim the module is built on — that both faces carry the same method set — and the device store'sclearhad it for the reason the account store needed it more: a reset written as a loop overall()andremovecan stop half way, and what it leaves is a set of preferences nobody chose, on the one store every device the user owns reads.Sends no body and no query, which is what lets the server do it in one statement, and answers nothing. Idempotent — clearing an account that holds nothing succeeds — so every non-2xx answer throws, a
404included.It sharpens the existing key check rather than adding one: the empty key resolves to the collection, where a
DELETEis now the clear, so validating a key before a URI is built is what keeps "delete one preference" from becoming "delete all of them". -
A preference can be moved between the two stores by one call (tcopen226).
TomDevicePreferences.moveToUserStoreandmoveToDeviceStorecarry one key across, each reaching the account store through callbacks asgetPreferreddoes. Both answer whether the destination holds the preference afterwards.No transaction spans a file on this machine and a row behind an HTTP round trip, so a move is at-least-once: the destination is written first and the source cleared second, an interruption between the two leaves the key in both stores, and repeating the call from there completes it.
The ordering is what makes that state harmless — both copies then hold the same value, so the precedence rule picks the value the move was carrying whichever copy it picks. The value in effect never changes. The same invariant decides the overlap case:
moveToDeviceStoreonto a device that already holds the key writes nothing and removes the shadowed account copy, because promoting the shadowed value would not move a preference but change the one the user sees. -
The device store holds the account store's 256-key ceiling (tcopen224). Every operation reads the whole document and every mutation writes all of it back — the trade that keeps the backend contract at two methods, and two methods is the whole cost of adapting a new platform. What was missing was the other half of the argument: nothing said where the trade stops working, so a caller who put a per-item preference in the store discovered the cost as jank rather than as a rule they had broken.
Half the bound existed already: the kernel codec refuses an encoded value over 16 KiB. The number of entries was unbounded.
TomStoredDevicePreferences.setnow refuses a write that would add a key beyondtomUserPreferencesMaxPerPrincipal, carrying the contract's ownpreferences.limit.exceededrather than a device-local spelling — it is one rule, so it has one name. Applying the account store's ceiling is not only symmetry: a preference is meant to be able to move between the two stores, and a device document the account store would refuse could only move one way.Only an insert is refused; replacing is never refused for the count, and removing one frees a place. The two ceilings make the worst case about four megabytes and a real document a few hundred bytes — the number
doc/preferences.mdnow states, mirroring the kernel's How big the account store can get.
Fixed #
-
An interleaved write is detected rather than lost (tcopen223). Every write is a read-modify-write over the whole document, so two writers over one location — two desktop windows sharing a support directory, two browser tabs on one origin — each read the same document and each wrote their own version of it. The second won entirely and the first change was gone with nothing raised anywhere. The io backend's write-then-rename makes a single write all-or-nothing against a torn file; it never ordered two writes.
A store compares before it writes and raises
device_preferences.store.conflict;TomStoredDevicePreferencesre-reads and re-applies once. Two writers touching different keys both survive, two touching the same key are last-writer-wins, and a continuously contended store surfaces the conflict after exactly one retry — a retry loop over a location somebody else is writing in a tight loop is an unbounded wait with no way out.The comparison is over the document's identity, not a version counter. A counter would have to live in the stored JSON, and a header in every document would retire the property that an unowned document is exactly the array the server's
/preferenceslist endpoint answers. Comparing what is in place against what was read answers the same question inside the two methods the backend contract already has.
Added #
-
TomEncryptedDevicePreferenceStore, and a plain statement of what the device store protects (tcopen222). The shipped backends write plaintext — a JSON file, onelocalStorageentry — and the module never said so. The documented examples are innocuous, butdevice.lastAccountis already an email address in the shipped example, and the store that is the obvious place for "whether biometric unlock is on here" is the obvious place somebody will also keep the thing biometric unlock protects.A decorator over the existing two-method backend seam rather than a fourth backend: encryption is orthogonal to all three, so it wraps the file store on desktop, the
localStoragestore in the browser and the memory store in a test, and none of them changes.Values only, stated as a limit. The
key,valueTypeandupdatedAtare stored as they were — a reader learns thatdevice.authTokenexists and holds aString, not what it is. Encrypting the keys would have to be deterministic for a per-key read to work, which leaks equality while looking as though it does not. A test asserts the limit as well as the protection.The framework names the cipher seam and holds no key. Key material belongs to a platform keystore, and reaching one is the platform dependency this module exists to keep out of the call site; and
tom_cryptoships no symmetric cipher — checked, not assumed — so encrypting here would mean a new dependency on every application's behalf, or a hand-rolled one.None of it is a credential store, and all three documents now say so.
Added #
-
A device store can name its owner, and refuses another's document (tcopen221). Separation between two accounts on one device was by construction — a per-account
location— and a correct construction was indistinguishable from a wrong one at run time: no warning, no log line, no assertion. The wrong install simply served the previous user's last-used account and biometric flag to the current one, on a screen that looked entirely normal.TomPlatformDevicePreferenceStoretakes anowner— an opaque string, absent by default. A store told one records it in the document and refuses one written under a different owner withdevice_preferences.store.wrong_owneron the first read. It is a check on the arrangement rather than a substitute for it: two accounts still get two locations.Absent is a value on both sides. An unowned document read by an owning store is a mismatch exactly as two different names are — a lenient rule exempting absence would wave through precisely the direction that leaks, since an unowned document was written by a build that did not separate accounts and could belong to anyone.
The interchange shape is unchanged. Without an owner the document is still the bare array the server's list endpoint answers, so "the same text moves between the two stores" holds where it was argued for; the
{"owner":…,"preferences":[…]}envelope appears only where an owner was asked for. Wrapping unconditionally would have been the smaller change and would have quietly retired that property.
Changed #
-
all()is unmodifiable on both preference faces (tcopen220). The device store returnedList.unmodifiable, the account client a growable literal, and neither consumer document mentioned mutability on either side — so the difference was discoverable only by trying it, at the one call site the symmetry exists for: moving a preference between the two stores is where a caller holds both results at once.Unmodifiable is also right on each face's own terms. A returned list is a snapshot of a store rather than a handle on it, and on the account side adding to it would change nothing on the server — a mutation that appeared to work and did nothing, which is worse than the
UnsupportedErrorit now raises.The promise is made where the list is built, so the store face and the contract face cannot disagree:
all()islistPreferences(). -
The API summary's store-face heading counted six over seven rows. It said "the same six the device store carries" above a table including
close(). It now says six plusclose(), and why the device store has no counterpart: there is no connection to release.
Changed #
-
One encoding rule for one DTO, and an honest failure when reflection is missing (tcopen219). The module encoded
TomUserPreferenceDtoby two opposite rules — the disk half field by field, the account half through reflection — in one package, with neither file acknowledging the other. The hand-written half justified itself on the premise that "a framework store must keep working in an app whose generator never sawTomUserPreferenceDto", and that premise is false: every entrypoint runsreflection_generatorand the generated seed covers the whole dependency closure, framework types included. Both halves reflect now.The timestamp rule stayed behind. Reflection parses a
DateTimewithDateTime.parse, which reads a stamp naming no zone as this device's local time — the one clock certainly not the writer's, for a document meant to move between devices.updatedAtis validated before an entry reaches reflection. The encoding side needed no such care, measured rather than assumed: reflection normalises aDateTimeto UTC on the way out. -
*.reflection_not_initialisedinstead of a misdirected failure. With reflection uninitialised, a valid stored document read asdevice_preferences.store.invalid_content, and a correct HTTP 200 carrying a valid preference read aspreferences.client.invalid_response— "The server did not answer with a preference". Both now name the configuration fault andinitializeReflection(), and a write fails before the request is made rather than after it.doc/preferences.mdstates reflection setup as the module's precondition.
Documentation #
-
Four documented behaviours that no test watched (tcopen218). Two had stopped being behaviours before they got one:
removeListenerno longer ignores its argument, andobservableisfinal, so reassignment is settled by the compiler rather than by a test. Both claims are already gone from the documents.The two that remained are now pinned, and both needed a probe subclass because each is a claim about work not done — and absence of work has no public reader. That
didUpdateWidgetleaves an identicalsubStatealone is a claim about cost: registering and deregistering on the same observable leaves the widget registered either way, so the test countsaddObserverandremoveObservercalls instead. ThatonNotifydoes nothing without a listener is invisible through rendering, so the trace line tells the two apart — the_listeners.isEmptyreturn comes before it.
Fixed #
-
Trace messages are built only when they are written (tcopen217). Both
onNotifyimplementations interpolated their message, soobservable.toString()ran on every notification whether or not trace output was on. For the containers this framework observes — aTomList, aTomClasswith registered members — that walks the contents, on every mutation, for the lifetime of the widget.The fix needed nothing new:
tomLog.tracetakes anObjectandTomLogOutput.convertToStringcalls aFunctionmessage inside every output's level check, so a closure defers the whole build. All four trace calls in the module are closures now rather than two of four — a module where the form is used situationally reads as if the choice were a matter of taste.A test measures it: a
TomBoolsubclass counting its owntoString()calls records zero across notifications with trace off, from both the widget and the adaptor, and the message is still produced with the level raised. -
setStateno longer carries the log line. Its callback contained only the trace statement.setStatebrackets a state mutation, and the mutation here is external — the observable already changed. The callback issetState(() {})now, which says what is actually happening: record the notification, then mark the element dirty.
Added #
-
TomObservingWidgettakes akey, and it is documented as the lever it is (tcopen216). It was the only public widget in the package a caller could not key. The parameter arrived with the constructor rewrite for the hoistedchild; what this adds is the behaviour it selects, and the tests for it.Whether Flutter reuses an element or builds a new one is what chooses between
didUpdateWidgetmigrating the registration andinitStatemaking a fresh one. Only the first was reachable. The second is the normal wish when a list row rebinds to an unrelated model — a cleanState, not a migrated one — and its absence also kept the widget out of keyed and reorderable lists. -
A deregistration test that the
mountedguard had made vacuous. The guard added in the previous change suppresses the symptom the old test asserted — no exception after unmount — so it passed whether or notdisposederegistered. Found by mutation, not review. The replacement counts the notifications that reach theStatethrough a subclass, because a removed observer is never called at all.
Fixed #
-
TomObservingWidgetState.onNotifyguards onmounted(tcopen215).disposederegisters, so the ordinary path was already safe; the window it could not close is the kernel's.TomObservable.notifyObserversiterates a snapshot taken before the loop, so an observer removed by an earlier observer'sonNotifyin the same round is still notified — and that was asetStateon a disposedState. It is now a no-op, which is the right answer: there is no element left to rebuild.The during-build case is deliberately left to throw. An observable mutated from inside a
buildproduces Flutter's "setState() or markNeedsBuild() called during build", and that message names the widget that performed the mutation. Deferring to a post-frame callback would make it work one frame late, every time, silently — trading a diagnosable failure for an undiagnosable one, and hiding an application-structure mistake rather than a framework limitation.doc/observing.mdstates the rule it implies: compute insidebuild, write outside it.Both halves are pinned, so neither can be quietly reversed — removing the guard fails one test, deferring the during-build notification fails the other.
Changed #
-
TomObservingWidgetgained a hoistedchild, and its builder is now calledbuilder(tcopen213, and the rename tcopen214 asked for). Every notification rebuilt the whole subtree the builder returned, with no way to pass a part that does not depend on the observable — the escape hatchValueListenableBuilderandAnimatedBuilderboth provide, and the one the case this module is for needs most: a form bound to a high-frequency observable repainted everything on every keystroke.buildernow takes(T state, Widget? child)and an optionalchildis built once by the parent and handed back on every rebuild. The rename was a prerequisite rather than a separate tidy-up: the conventional name for a pre-built subtree ischild, which the builder had, and in Flutter aWidget Functionis abuilder. The two could not both bechild.What makes the hoist work is the same widget instance travelling through every rebuild —
childis built in the parent'sbuild, untouched by this widget'ssetState, and Flutter skips rebuilding a child whose widget is identical to the one already mounted. So the saving is the subtree's whole element rebuild, not merely a closure call.
Changed #
-
ValueListenableObserveris now a real multicastListenable(tcopen212). It declaredValueListenable<T>and honoured neither half of the contract:addListenerreplaced any previous callback andremoveListenerignored its argument and cleared the single slot.addListenernow appends andremoveListenerremoves the callback it is given.The argument is substitutability. A
Listenableis structurally substitutable wherever Flutter asks for one — nothing at a call site distinguishes an adaptor that multicasts from one that does not — so the old behaviour left the first consumer silently not rebuilding, with no error anywhere to find. TwoValueListenableBuilders over one adaptor was the obvious case; a builder plus anAnimatedBuilder, or plus a manual registration in aState.initState, was the same failure arriving less visibly.Flutter's own corner semantics come with it, each pinned by a test: one callback registered twice fires twice and takes two removals to stop (a
List, not aSet, because the contract counts registrations); removing a callback that was never registered does nothing rather than silencing the adaptor; dispatch walks a snapshot, so a listener may add or remove listeners while being notified; andremoveListeneris tolerated afterdisposewhileaddListeneris not, because a widget tearing down cannot always control whether the listenable it registered on was disposed first.The single-listener callouts are gone from all four artefacts rather than softened, and a guard reads them to make sure they stay gone — a document still warning about a single slot would send a reader to a workaround for a problem that no longer exists.
Changed #
-
ValueListenableObserver's two fields stopped being public surface (tcopen211).observableisfinaland the listener slot is private.Assigning
observablenever retargeted an adaptor — it produced one that was internally inconsistent. The constructor's registration stayed on the old observable, soonNotifykept firing from the observable the adaptor no longer reported whilevaluereturned the new one: aValueListenableBuilderrebuilding on one observable's changes and rendering another's contents. Retargeting is what a new adaptor is for;TomObservingWidgetmigrates instead, and can, because a widget has adidUpdateWidgetto migrate in.The listener slot is implementation state. Assigning it bypassed
addListener, which is where the single-slot replacement is documented and traced and where use afterdisposeis caught.valueis the reader, and the API summary's property table now says so.Making the slot private cost the suite its previous probe, and the replacement is better: the test that tells
disposefromremoveListenercounted on refilling the slot directly, and now countsonNotifycalls through a subclass — which measures the thing that actually differs, whether the adaptor is still reached, rather than a field that happened to correlate with it.
Added #
-
ValueListenableObserver.dispose()(tcopen210). The adaptor subscribed in its constructor and had no way to unsubscribe:removeListenerclears the Flutter listener and leaves the kernel registration in place, so a silenced adaptor stays on the observable's observer list and forwards again as soon as the slot is refilled. The only thing that ended a subscription was the kernel's weak-reference sweep — which fires when the adaptor becomes unreachable and something else happens to notify, not at a moment the caller can choose, and an adaptor held in a field never becomes unreachable at all.dispose()deregisters and drops the listener, and is required for any adaptor held beyond a single build — the counterpart of theremoveObserverthatTomObservingWidgetStatealready performs in its owndispose. It is idempotent, because aStatethat disposes and then nulls its field ends up calling it twice; andaddListeneron a disposed adaptor fails anassertin debug, because a wired builder that silently never fires is the shape that wastes an afternoon.tom_uam's login page moved with it: it constructed an adaptor insidebuild, subscribing afresh on every rebuild and ending none of them, and is now aStatefulWidgetholding one adaptor and disposing it.
Documentation #
-
Six behaviours the documents describe now have a case (tcopen209). The suite pinned constructor forwarding and the type relationships, which is the module's core, and stopped there. Added:
toString's format, asserted by its parts rather than as one literal so the kernel stays free to reorder them;logRepresentation's equality with it, pinned precisely because it is equal by construction and two silently diverging renderings of one failure is the failure mode; both arms of thestackboundary, including thedynamicnon-trace that is refused at the call site rather than inside the constructor; andTomClientUIException'sautoLogdefault — the arm that travels through twosupercalls carrying an inherited default value, checked for its silence as well as its flag, since a default that arrived asfalseand logged anyway would pass a field check.The post-construction-assignment case now covers all seven writable members rather than four, and a companion case reads
finaloff the kernel source for the two that are not: "you cannot assign it" is the one assertion a test cannot make by assigning it. -
serverCallErrorwas described as settable and isfinal. So isautoLog. The class dartdoc,doc/little_things.mdand the completion document each said all three identifying arguments were writable fields as well as constructor arguments; forserverCallErrorthe constructor is the only route there has ever been. Corrected in all three, and the Limitations bullet about the writable payload now names the two exceptions to it. -
doc/little_things.mdnow carries a Limitations section (tcopen208). It is the document a consumer of the package reads and it stated no constraint anywhere; the list that did exist was quest-internal. Of the four the todo named, none survived re-measurement: two were resolved, one was false, and one had become a stated contract with its own section. The section is therefore written from what is true today — the types add no behaviour beyond the distinction; clause order is load-bearing and gets it wrong silently; the message is not resolved here;autoLogwrites at construction rather than at throw; the payload stays writable after construction andparametersis held by reference; a wrap does not share its cause's identity; andserverCallErrorhas one producer in this package. Each is executed by the suite rather than asserted in prose. -
The
stackfield and parameter no longer differ, and seven artefacts said they did.tom_basics3.1.1 declaresStackTrace? stack, the same widthTomException's constructor accepts, so the standing illustration of "super-parameters resolve against the superclass constructor, not the field" had stopped being one. The rule is unchanged and still load-bearing, so it keeps a live example —uuid, whose field is a non-nullablelate Stringwhile its parameter isString?— across the four API summaries, the shared constructor-mirror helper, and the two test files that explain the guard. -
Message resolution is now a stated contract with a mechanism, not an open item (tcopen207). Every artefact said
keyis what a UI resolves against the resource layer anddefaultUserMessageis the fallback, and every one of them added that nothing enforces the order. It stays the UI layer's job — an exception is raised where a resource tree may not exist yet (early start-up, a worker isolate, a unit test) andgetAppResources()throws there rather than degrading quietly, so a type that resolved on construction would carry that failure into the situations where reporting one matters most.What changes is that the contract is written down and executed. The class dartdoc,
doc/little_things.mdand the module readme all show the four-line lookup, and all three namegetTextOrNullrather thangetText— the latter echoes the key on a miss, so a helper written with it showslogin.failedto a user and discards the fallback entirely.The example runs the lookup in both its states and prints what
getTextwould have shown instead. The suite pins the other half: with a resource entry present under the exception's own key,defaultUserMessagestill answers what the throw site wrote, and the source is read to confirm it names no part of the resource layer.
Fixed #
-
A client failure could not name the request it belonged to (tcopen205).
TomClientExceptionandTomClientUIExceptionforwarded six of the kernel's nine constructor arguments while both their dartdoc anddoc/little_things.mdcalled the payload "the same". The three missing ones —uuid,requestUuidandserverCallError— are the ones that say which failure this is rather than what it was, and each had a case the client could not express: a failure rebuilt from a server's error envelope has to keep the identity the far side logged it under; the client is the side that issues requests, so it holds the correlation id; and a client is where outbound HTTP calls originate.All three are forwarded now, matching the decision already taken for
TomServerException. They are constructor arguments rather than fields assigned afterwards — all three were always writable, but a report edited between the throw and the catch describes a failure the code in between decided on.They were given a producer rather than shipped reachable-but-unused.
TomUserPreferencesClientnow builds aTomServerCallErrorfor every rejection and for every answer it could not decode, and copies the server'srequestUuidout of the framework error envelope onto the exception — so both sides of one failed call name the same request. It does not borrow the server'suuid(this exception was raised here, not rebuilt), and it reads no id out of a body that is not an envelope, because a wrong correlation is worse than none. -
The construction-order case was documented as a silent failure and was not tested (tcopen204). The completion document's open list said a holder built before its resource providers left "every setting silently sitting on its declared default". That has not been true since
getAppConfig()stopped bootstrapping itself: the holder throwsresources.config_provider.not_installedfrom its own constructor, naming the remedy. The claim is corrected, the loud failure is now stated indoc/configuration.md, the module readme and the holder's dartdoc, and a test pins both the refusal and the fact that the remedy the message gives is sufficient.
Added #
-
Composition of the three client value scopes (tcopen203). Every configuration artefact deflected an out-of-scope value to "a user-preferences holder" or "a user-profile holder" without naming one, though both ship in this package through this barrel — and nothing composed them, so a caller who wanted "the user's override of the install's server URL" wrote the fallback chain by hand at every call site. The CodeSpecs model for this scope carries a required, undefaulted
CsClientConfig.overridableBy, so the relation was modelled and had no runtime to land in.The four declaration methods take
overridableBy:, andTomEffectiveSettingsapplies it: the narrowest store holding the key answers, failing that the next, failing all of them the setting's own value. Shadowing is opt-in and authored at the wider scope — the default isTomOverridableBy.none, so a preference sharing a key with a pinned setting is never read rather than merely outranked, and the permission is stated once with nothing to disagree with it. The arm names the narrowest scope permitted and every scope between is permitted too, sodeviceopensuserwhileuserdoes not opendevice.Both stores arrive as
TomPreferenceLookupcallbacks rather than as dependencies, for the reasongetPreferredalready gives: an application with no account store must not be made to carry an HTTP client.TomDevicePreferences.getandTomUserPreferencesClient.getare each one as written, and an omitted lookup is never consulted.A setting that opens itself is validated against
TomUserPreferenceKeysat declaration and refused withpreferences.key.invalid, because a key the stores would reject makes the declaration a promise nothing can keep. The environment layer is independent of all of this: layer 0 decides the install's value,overridableBydecides who may shadow the install.There is no composed write and no composed observation, and both absences are decisions: a value is written by whichever holder owns its scope, and a preference has no change notification to re-emit.
-
An environment layer for client settings (tcopen202). Resolution had three layers — persisted override, shipped config resource, declared default — and no way for an operator to configure an install. That is the module's own headline use case: every artefact opens with "the server URL to talk to ... an installer can point at a different environment", and an environment variable is how an installer or a CI job does that without editing a file or shipping a build. The CodeSpecs model for this exact scope already named the field (
CsClientConfig.envAlias), so the runtime was behind a decision that had been made.The four declaration methods take
envAlias:, and a variable the reader reports set becomes layer 0 — above the persisted override, because the two answer different people: an override is what this install chose, a variable is what the operator who launched it decided. A layer the install could quietly out-vote would not configure a deployment.Four consequences follow from that placement rather than being separate rules.
save()never writes such a setting, so unsetting the variable restores the layers below instead of finding the operator's value baked into the store;isOverriddenisfalsefor one, so it is absent fromhasOverridesandhasUnsavedChanges;reset()restores the operator's value, since "drop the install's override" cannot mean "drop somebody else's decision"; andload()restores it too, becauseload()produces the state a fresh start would produce.TomClientEnvironment.readeris where the value comes from, selected through the samedart.library.ioconditional as the file store — the process environment where there is one,nullwhere there is not. A browser has no environment and a mobile app is launched by the OS; those platforms answer the question with a--dart-define, a startup fetch or an MDM payload, and only the app knows which, so the reader is replaceable rather than the framework picking one source and calling it the environment.A value counts as set when the reader answers non-null, the empty string included —
APP_SERVER_URL=is an operator saying "present and empty". It is coerced like any other external value, soAPP_RETRIES=abcagainst anintsetting throws at declaration, where it names the key. -
A persisted value equal to the baseline no longer reports a holder as dirty (tcopen201).
hasUnsavedChangescounted such an entry, becausesave()would indeed drop it — so a freshly loaded, untouched holder answeredtrue, which is the warning the getter was added to stophasOverridesproducing, reappearing for one input. It now agrees withisOverriddenand withsave()about what an override is: a persisted value that reads as the baseline (coerced, since the store may spell3as'3') is not one. A persisted override the user has reset back to the baseline still is, and that distinction is what the comparison turns on. -
hasUnsavedChanges(tcopen199). There was no way to ask whether anything was unsaved, and the obvious substitute is wrong in the direction that warns every time.hasOverridesmeans "differs from baseline" — a fact about the install — so a settingload()adopted from the store is already overridden with nothing unsaved, and a screen guarding its dismiss withif (hasOverrides) warn()warns on a holder that was loaded and never touched.doc/configuration.mdinvited exactly that reading, labelling it "whether the install has changed something".The holder already kept the answer:
_persistedis what the store last held.hasUnsavedChangesistrueexactly whensave()would write something else — a value changed, an override added, or an override dropped, since a removal is unsaved work too. It reads no store, so it is answerable in a build or a dismiss guard; keys this build does not declare are never unsaved, becausesave()carries them forward untouched; and with no store wired any override reads as unsaved rather than throwing, because a dismiss guard has to be answerable.save()and the new getter now build the override map through one private helper. Two copies would be two answers to "is there anything to save", and the getter would eventually disagree with the writer it exists to predict.hasOverrideskeeps its meaning and its name and says in its own dartdoc that it is not a dirty flag.
Documentation #
-
The pin an install cannot make (tcopen201). Only overrides are stored, so a shipped default can still move in an app update — the module's central decision. Its consequence was written down nowhere: a stored value that merely equals the baseline is not an override either, so it survives
load()and is deleted by the nextsave(), after which the setting follows whatever the shipped default becomes. "Keep this build's refresh interval whatever the next update ships" is a real operation, and the module silently turns it into its opposite, with no error and no signal.Accepted rather than closed: pinning needs a third state per setting — untouched / set-to-baseline / overridden — and a store format able to carry it, which is a real format change for a use case nobody has asked for. What changes is that
doc/configuration.mdLimitations, thesave()dartdoc and the completion document say so, andtest/configuration_test.dartpins the behaviour — including that asave()which starts keeping the key turns the suite red rather than leaving the documentation describing a limitation somebody has lifted. -
What sharing a store location actually costs (tcopen200). The module documented the hazard as a narrow race — "re-reading means a holder preserves keys another writer has added", exposure "one store round-trip rather than the whole session". Measured: true for a key another build owns, false for a key both holders declare. A second holder that has not adopted the first's change holds that key at its baseline, so its view is "this install has no override" and that is what it writes. The first holder's value is erased sequentially, with no concurrency involved.
The behaviour is unchanged and is the design:
save()writes this holder's own complete view of the keys it declares, which is what keeps the holder and the store in agreement and what keeps shipped defaults unfrozen. What changes is thatdoc/configuration.md, thesave()dartdoc and the completion document now say so, andtest/configuration_test.dartpins all four cases — the foreign key that survives, the declared key that does not, the reload that makes sharing workable, and the interleaved save that loses a change even when both holders reloaded.Closing it properly means
save()writing only the keys this holder changed, which the snapshot added alongsidehasUnsavedChangesnow makes possible. It is recorded as an extension rather than done, because it costs asave()that is no longer idempotent against an externally edited store and a holder that can disagree with it silently — the right trade only if shared locations turn out to be supported rather than accidental.
Documentation #
-
The single load/resetAll notification survives a caller's own mute (tcopen197). A screen batching a multi-setting edit around a reload mutes the holder itself and calls
load()inside its batch, soload()'s mute is an inner one. That the inner release does not free the caller's batch is a property ofTomObservable.mutecounting depth rather than setting a flag — a property this module cannot enforce, since it lives in another package.Were it a flag, the caller's suppressed writes would escape during the load, the holder would come back unmuted without anyone asking, and every later write in the "batch" would notify immediately — a screen asking for one rebuild getting two or three.
test/configuration_test.dartnow asserts the consequence from the outside, for bothload()andresetAll(), and both mutations were run: the kernel'sunmutedropping to zero instead of decrementing, and this module releasing one mute too many.doc/configuration.mdsays so where it states the one-notification property.
Fixed #
-
A preferences rejection now says why (tcopen54). A
preferences.client.rejectedfailure carries the server'serrorKeyin itsparameterswhen the answer was the error envelope, so a caller tells an account at its ceiling (409,preferences.limit.exceeded) from a value the server would not store (400,preferences.encoded.*) without parsing the body. Both stores refuse a value over the kernel codec's 16 KiB, the client before it makes a request. -
TomUserPreferencesClientread a refusal as "not set" (tcopen53). Any404from the read answerednull, and any404from the delete answered success. But the endpoint pipeline answers a denied or signed-out caller with a bare404, so a signed-out user was silently handed the defaults, and a refused delete was reported as done — while the server deletes a missing preference with a200, so a delete404was never "already gone".Absence is now only a
404whose envelope names the kernel'stomUserPreferencesNotFoundErrorKey; every other404throwspreferences.client.rejected, on the read and the delete alike. The example's loopback server answers a miss the way the real one does. -
A declared setting read the configuration twice (tcincb164).
_declaretestedexists(key)and then readgetObject(key), which are the same resolver —_resolveObjectOrNull— so every declared setting walked the nested map twice to answer one question, at start-up, for every application. It is now onegetObjectOrNull.The cost was small; the trap was not.
getObjectanswers a miss with the key you asked for, so theexistsguard was the only thing standing between a missing key and aStringsetting holding the literal'client.server.url'— a value that coerces cleanly and is stored as though it had been configured. A silent wrong value, not a failure.getObjectOrNullcannot produce it, so the safe path no longer depends on remembering the guard, which is what the kernel's own dartdoc ongetObjecthas been advising.Behaviour is unchanged, including for a key present with no value: both accessors resolve through the same walk, so it reads as absent either way and still takes the declared default.
configuration_test.dartgains the regression the guard was silently providing, against a fixture declaring oneStringsetting and nothing else — with several types declared together anintthrows a type mismatch first, so the assertion about the value of this one is never reached and the test fails for the wrong reason. -
The README's
## Statusblock stated a test count (tcincb153). "285 tests — 8 module suites" was a claim nothing checked, and every added test would have been a README edit nobody makes. The block now describes what the suites cover and gives no number; the version beside it is pinned topubspec.yamlbytom_ai/core/tool/check_readme_status.py. -
A whole number configured for a
TomDoublePropertythrew instead of widening (tcincb107). No configuration format can say "double":rate: 2in YAML and"rate": 2in JSON both decode to anint, so a value that was exactly what the operator meant failed the property declared to read it — while the reflected configuration path fills a declareddoublefield from the same entry. One file could fill a field and fail the matching property. The server'sTomConfighad already been corrected the same way, and the two are read by the same operator writing the same file.A failed cast onto a
Tthat accepts adouble, from a value that is anint, now widens. It sits after the cast rather than in front of it, because0.0 is Tis also true fornum,Objectanddynamic, where2 as Talready succeeds and theintmust survive as one — only a cast that has actually failed can have been aimed at adouble. It does not narrow (2.5under aTomIntPropertyis still a mismatch — narrowing needs a rounding policy) and it does not parse ('2'is still a mismatch).The rest of the sibling's repair was already in place here: the catch is
on TypeError, the failure is aTomPropertyException extends TomClientExceptioncarryingkey,parametersand theTypeErrorasrootException, and the value is read once. A test now covers the case that makes the chained cause load-bearing rather than tidy — a configured['alpha', 'beta']read asList<String>fails on the element type, and the message reportswith value [alpha, beta], which is what the operator wrote and looks perfectly valid. Only theTypeErrornames both types.
Removed #
-
TomClient2FAAdaptor, and with itlib/src/tomclient/security/authentication.dart(tcincb1_ahzv). The annotation was exported from the barrel, carried dartdoc, held four tests, appeared in the example and had a section in every document the module publishes — and nothing inlib/read it. Nothing anywhere read it: the flow finds a panel throughTom2FAClientRegistry, and no Tom runtime enumerates client-side annotations. It described a dispatch route that did not exist.An unread annotation is not inert. A reader who finds one in a barrel reasonably concludes that annotating a class does something, ships a mechanism annotated and unregistered, and discovers at the chooser that their mechanism is absent — the API having promised registration it never performed. So the type is gone rather than documented-as-decorative:
Tom2FAClientRegistry.registeris now the only route into the registry, and it is the only one the module ever had.The server's
@Tom2FAAdaptoris unaffected —tom_core_serverdoes scan for it, so it dispatches on what it declares. The two tiers are deliberately asymmetric: the server discovers adaptors, the client registers mechanisms.Nine public types remain in the module:
Tom2FAChallengePanelContext,Tom2FAClientMechanism,Tom2FAClientRegistry,Tom2FAEnrolmentPanelContext,Tom2FAFlowController,Tom2FAFlowPanel,Tom2FATexts,TomQrCodeViewandTomTotp2FAClientMechanism.test/security_test.dartkeeps only the example run and no longer needs generated mirrors, so it leavesbuild.yaml'sgenerate_forand its*.reflection.dartcompanion is deleted.
Documentation #
-
doc/api_reference/api_summary_resources.mdnever showedTomPropertyException's constructor. The page documents the two keys it raises and whatparameterscarries, so the omission read as covered — a class whose name is all over a page looks documented whether or not anyone can see how to construct it. Found by the generalised coverage guard above, which is the one summary of the seven it caught. -
12
Kindcells across three documents described the symbol instead of declaring it.TomDevicePreferencesandTomDevicePreferenceStoreboth readinterface— a word Dart does not have — where both areabstract class, which is precisely the difference between "implement this" and "you may also extend it". The rest were role words (exception,StatefulWidget,value class,static registry) that thePurposecell beside them already conveyed.Each cell now carries the declaration.
Tom2FAClientRegistryandTom2FATextsnote inPurposethat they are entirely static — the fact the oldKindcells were smuggling.
Added #
-
No artefact may cite a document in the AI-state mount (tcincb163).
test/test_comment_hygiene_test.dartgains a package-wide check for it. Two test comments used to cite section 6 of the completion corpus's contract for their rationale; that file exists, but only intom_agent_state, a different repository symlinked into the workspace — so a reader holding these sources and not that layer could not follow the reference at all. It was the least followable shape a citation can take, a bare relative-looking path that reads like a sibling file and is four directories away in another repo. The two comments were inlined earlier; nothing stopped the next one.Citing another package in the code tree stays allowed, and the check is written so that it is:
codespecs_mapping.mdships with a checkout of the monorepo, and three artefacts here cite it deliberately. The needle is anchored for the same reason — as a plain substring it matched three files citing this repository's own tooling undertom_ai/, which is exactly the reference the rule permits. -
test/api_reference_coverage_test.dart— the API-summary coverage guard, generalised from one module to all seven. It was written forconfiguration, where six public members were missing from the summary and nobody noticed until somebody looked; the other six modules kept the same unverified mirror. The table is derived by walkinglib/src/tomclient/, the same authoritymodule_inventory_test.dartuses, so a module added tomorrow is covered without anyone widening a list.Generalising the parser meant fixing it. Attributing each member to the type declaring it stopped
_PreferencesResponse.body— a public-looking field on a class the module never exposes — from being demanded of the preferences summary. Skipping the framework hooks stoppedbuild,createStateandpaintfrom being demanded of the security summary, where the only way to satisfy that is to document a hook as though it were API. And a field declaredWidget Function(T) childreportedFunction, because the pattern stops at the first parenthesis; losingchildis the tolerable direction, demandingFunctionof a summary is not.The exception-parameter mirror is generalised with it, each class matched against the file that declares it —
constructorBodyFromfinds a constructor by looking forName(at a line end, which a multi-line call site matches as readily as a declaration, so handing it a whole module lets athrowthree files away stand in for the thing being documented. -
test/formatting_test.dart— the guard that keeps the packagedart format-clean. Twenty-one of its files carried the pre-3.7 short style until they were reformatted in one whitespace-only commit; nothing then held that state, andflutter analyzecannot see it. The observed response to the drift was the bad one — the formatter was avoided and new code written to match a file's existing style, which grows the drift rather than stopping it. The suite walks the package rather than reading a list of directories, so a file added undertool/is covered without anyone widening it, and it asserts that the walk found something rather than passing over an empty sweep.It cannot be the sibling guard copied across. Under
flutter test,Platform.resolvedExecutableisflutter_tester, notdart— the non-Flutter packages' version spawns the test engine withformatas its first argument and hangs until the suite times out, failing for a reason that has nothing to do with formatting. This one resolves the SDK'sdartthroughFLUTTER_ROOT, which thefluttertool sets for every command it runs. -
test/bearer_auth_barrel_export_test.dart, which holdsTomClientJwtTokento being nameable through this barrel (tcincb13). The type istom_crypto's; it reaches a Flutter application across two blanket re-exports —tom_core_kernel's oftom_crypto, and this package's oftom_core_kernel— andTomBearerAuthenticationreturns it from two public getters. A consumer therefore has to write the name to hold what it gets back, whichtom_uam_clientdoes:static TomClientJwtToken? clientToken;, withtom_core_flutteras its only Tom import.The existing coverage could not have caught the loss.
preferences_test.dartalready drivesTomBearerAuthentication.setTokensthrough this barrel and would keep passing with the type unreachable — a call site never has to write the type of what it ignores. The new file writes it, importing nothing but this barrel, so narrowing either re-export stops it compiling instead of silently shrinking the surface. Its first test reads its own source and holds the import list to that one barrel, since a straytom_core_kernelimport would leave the guard green and empty. The kernel carries the twin guard for the other re-export in the chain. -
lib/src/tomclient/configuration/readme.md— the one artifact the module lacked. Its six siblings each carry a module readme;configurationshipped with dartdoc, a doc page, an API summary, an example and a suite, but no readme, and no inventory noticed. Covers the(client app, machine)scope and why a per-user value belongs inpreferences/instead, the three-layer resolution order and why only the override layer is persisted, and the three deliberate non-features (no validation, no autosave, no concurrency control). -
test/module_inventory_test.dart— six census checks that hold every module inventory level withlib/src/tomclient/: the guide's Target Modules table (set equality both ways), a per-module checklist step for every live module, the barrel's exports, each module's five artifacts, no artifact outliving its module, and every bolded number word in the guide equalling the current module count.The existing
example_instructions_test.dartcould not catch any of this: its own comment calls its module list "a floor, not a census", so it fails on a deleted module and never on an added one — and an addition is exactly the change that leaves hand-written lists behind. Each check was falsified before being trusted (a planted module directory trips five of the six; a table row for a non-existent module, an orphaneddoc/*.md, a removed module readme, a stale bolded count and a dropped barrel export each trip their own). That exercise found a real defect in the test itself — the barrel pattern was unanchored, so a commented-out export still counted as an export; it is now anchored to the line start and both the commented and deleted forms fail. -
A second check in
tool/check_dartdoc_refs.py— a backticked capitalised name on a doc line that frames it as past ("legacy", "replaces", "retires", "no longer") must appear somewhere in repo code. This is the fault below, generalised: the existing gate deliberately judges only bracketed private references, because a dangling public[Ref]cannot be told apart from a correct one missing an import. Backticks remove that ambiguity by construction — dartdoc never links them — so "does this name exist at all" is answerable, and past-framing is what turns a backticked name into a claim worth checking.Both the line-vs-block scope and the framing vocabulary were settled by measurement, and both are recorded in the module docstring. Judging whole comment blocks reports 8 names of which 6 are wrong (
O_EXCL,QRTZ_JOB_DETAILS,ClassName…), each sitting in a paragraph whose framing word is about something else; per-line judges 21 and reports 2, both real. Addingreplaces/retiresto the vocabulary took the judged population from 3 to 21 and found a second genuine fault with no new false positive. Falsified in six directions before being trusted: it fires on the original fault and on a novel name/framing pair, and stays silent for a framed name that exists, a vanished name with no framing, and a framed vanished name inside a fenced code block. -
A second check in
tool/check_doc_source_paths.py— a backticked module directory (`lib/src/.../x/`) named in package documentation must exist on disk, or be named in the tom_core completion corpus, which is where this repo records removals. The existing gate deliberately restricts itself to**Source:**lines because naming a removed directory in prose is legitimate; the fault below is what that carve-out cost. So the bar here is coverage, not existence — a directory that is gone may be named freely, provided the record accounts for it. Neither lookup is a judgement, so the rule adds no triage.Measured before being switched on: 97 directory claims across the repo, of which six sites name five absent directories (
camera,forms,localstorage,qrcode,iding), all five covered — zero faults on the current tree. Run against the completion corpus as committed before the fix below, it reports exactly one fault: the real one.Falsified in ten directions against the shipped
audit_module_dirs, not a reimplementation. Five of those directions exist because a draft resolved package-prefixed claims wrongly, and the sequence is recorded in the module docstring rather than quietly fixed, because each wrong version passed everything it was tested against at the time. Draft one used the repo root alone and reportedadvanced_ops_server/lib/src/jobs/missing — packages nest, and that one sits undertom_core_samples/. Draft two walked the document's ancestors, which resolved it from the ops README and still failed from this CHANGELOG, a fault this very entry exposed within minutes of being written by citing that directory fromtom_core_flutter. Where a claim is written has nothing to do with where the package it names lives, so the shipped version indexes packages by name.Separately, the first falsification harness passed while testing nothing: it reverted only the removal record, but the fix had also added the name to the corpus overview and README, which it read from disk — so the name was still present and the rule stayed silent for the wrong reason. The harness now takes the whole corpus from the revision under test.
Fixed #
-
The two faces of the preference store listed the same preferences in two different orders (tcincb1_ahzg).
TomUserPreferencesClient.all()answers in the server's order — key ascending, stated on the endpoint since tcinc613 — whileTomStoredDevicePreferences.all()returnedList.unmodifiable(await store.read()), which is whatever sequence the backend happened to hold. The module's own documentation says in three places that a preference can move between the two stores without a call site changing; an application that renders either list, or diffs one against the other to decide what to sync, could not do so without knowing which face it held.The device store now sorts by key on the way out. On the way out, not on the way in:
TomDevicePreferenceStore.read()explicitly permits a store to answer from a cache, a keychain or a platform settings panel, none of which has an insertion order to preserve — so a sortedset()would place the guarantee exactly where a hand-edited document, an older build or a third-party backend can bypass it, and it would make a one-key write reorder the document around it.TomUserPreferencesClientdeliberately does not re-sort: the order is the server's to produce off an index that already covers it, and a defensive sort would pay for itself on every call in order to hide the one symptom by which a store breaking the contract could be noticed.The old dartdoc called the sequence "storage order", which named
set()appending rather than anything a caller could rely on. The rule now lives in one place —TomUserPreferencesApiService.listPreferencesin the kernel, the declaration both faces implement — and a doc-hygiene group holds the five artifacts (both dartdocs, the module readme, the doc page, the API summary) to stating it and to no longer promising the old one. -
The removal record paraphrased away both halves of a cross-reference the two bookmark readmes made to each other (tcinc1115). One sentence in the record summarised the deleted
qrcode/readme.mdas "three bullets — generate, scan, scan-from-picture". The count was right and the shape was wrong: it split bullet 1 ("Generate and scan QR codes") into two items and dropped bullet 2, which was "Camera integration?". That bullet is the one asking for the capability the record's own Camera → not provided section and the first entry of What is still open both answer with "no", so the paraphrase deleted the evidence tying the two removed bookmarks together.The fault was larger than the one sentence. The neighbouring
camerasummary named two of that readme's four bullets, and one of the two it dropped was "check how to link this with QR-code recognition" — the other half of the same reciprocal reference. Fixing only the half named above would have left the identical loss one clause earlier. All sevencameraandqrcodebullets are now quoted verbatim,localstorage's four headings and eight links are stated as such, and a closing paragraph says plainly that the two bookmarks were asking one question, not two.Drafting it reproduced the fault being fixed: the first
localstoragesentence enumerated six links where the file has eight. It was caught by counting the file rather than re-reading the sentence — which is the only method that works here, and the reason the section now carries agit show 3bdb9f0:./lib/src/tomclient/<dir>/readme.mdpointer. This is the sole document in the completion corpus whose subject is deleted files; everywhere else a reader can open the source, and only here does a quotation have no reachable original to be checked against.No gate was added, on measurement. A rule judging quoted fragments against repo content finds 1002 across 75 documents, 246 of them four words or longer, and the overwhelming majority of those 246 are the author's own rhetorical questions ("can I serve traffic", "where do files live") which have no source to match. ~240 false positives on a clean tree puts it in the same band as the cross-library dartdoc rule (353), "name every public type" (519) and markdown line-citations (173) — all three declined here for the same reason.
-
The removal record covered four removed directories; a fifth was invisible to the method that built it (tcinc1114). The record scoped itself to "four directories that are no longer part of
tom_core_flutter" — and for a record built by diffing the baseline tree against HEAD, four is correct: exactly four tracked directories were removed.lib/src/tomclient/iding/held no files, and git tracks files, not directories, so it appears in no commit on any branch. A tree diff is a census of tracked files, not of directories; the diff answered the question it was asked.That is not a comfort to a reader.
idingis named five times in documentation that outlived it — four indoc/review_findings.md, once in_copilot_guidelines/refactoring_guide.md— and this record is the designated answer to "where did this go?". The record now covers five directories, with a table row and a dedicated section stating that it held nothing, has no successor, and why no git-based detector could ever have found it. Three sibling scope claims were swept with it (the completion overview's name list and its own four-row table, and the corpus README), so nothing contradicts the record it links to.One claim in the first draft of the fix was wrong and was caught by measuring it: "what it was for is not recoverable" is false — a staged guideline in
tom_specsdescribes it as "Identity and identification utilities". The row now carries that description together with its provenance: a table that predates every removal, corroborated by nothing else. -
Two completion records promised "verbatim" for blocks that were not (tcinc1113). The removal record introduced its
AdminLoginFormblock asadvanced_authadmin_client/lib/forms/admin_login_form.dart"verbatim, minus its dartdoc", but the block also droppedimport '../domain/admin_login_credentials.dart';, without whichTomForm<AdminLoginCredentials>has no declaration for its type argument — so the snippet a reader is invited to copy does not compile as printed. Diffing the block against the file line by line found three deltas, not one: the missing import, thelibrary;directive, and an inter-field blank line. The latter two are legitimate consequences of removing dartdoc (library;exists only to carry it; the blank exists only because each field has it), so the import was the sole real defect. Both DONE WHEN branches are now satisfied rather than the cheaper one — the import is restored and the introduction names what was elided.The endpoints record carried the same claim shape and was checked the same way: its readme quick-start is "verbatim identical, minus dartdoc" to the
TodoApi/TodoApiServerpair intest/fixtures/reflection_fixture.dart. Measured,TodoApiServeris identical andTodoApidiffers by one dartdoc line; the wording now says so.No gate was added, and the numbers say why. Two detectors were built and measured before declining. A prose detector (a
dartfence preceded within six lines by "verbatim" plus a backticked resolvable.dartpath) raises 8 candidates for 1 true positive — one false positive comes from a "verbatim" sitting in an unrelated table row six lines above an unrelated fence, which only natural-language judgement separates. A content detector (an inverted line index over 1499 files identifies the reproduced file by ≥80 % line overlap, then requires the block to carry that file's imports) reports 5 blocks on a clean tree, and both firings inspected are excerpts that never claimed to be reproductions — one is introduced with "this is howtest/runtime_test.dartreaches it", the other is a readme Quick Start.tool/check_source_citations.pygates a population of one on the argument that "the rule fires zero times on the current tree, so switching it on adds no triage burden at all"; it can afford that because its detector is syntactic (foo.dart:214). Neither detector here is. This is the same noise ground on which this repo already declined the cross-library dartdoc rule at 353, "name every public type" at 519, and markdown line-citations at 173 — a principled decline, not an unexamined one. -
A shipping sample's dartdoc oriented the reader against deleted types (tcinc1112).
advanced_authadmin_client/lib/forms/admin_login_form.dartdescribed its fields as "not the legacyTomFormOld/TomEmailFieldAPI" — both removed withforms/in 1.1.0. Backticks rather than brackets meant dartdoc emitted no broken link and nothing failed to build, which is why it survived; the cost fell on the reader, told what the code is not by appeal to something they cannot look up, in the sample a newcomer copies. The sentence already said positively what the form is, so the contrast simply went.The new gate then found the same fault twice more in
advanced_app_server, where prose named a bespokeResourceAccessResolverandShareRecordthat exist nowhere in the workspace; both now describe what they replaced without naming a type the reader cannot reach. Two further copies intom_uam/tom_uam_client/lib/uam/sample_app_state.dartwere fixed in the same pass — one of them (the "modern replacement for the legacybloc" comment) had not been filed. Where the contrast was load-bearing, the positive reason replaced it: the login form is a lazy getter because reflection must be initialised first, which is a fact about this code rather than about a deleted one.TomFormOldnow survives only in the CHANGELOG entries that record its removal. -
Four module inventories in the refactoring guide disagreed with the folder (tcinc1111). The
## Target Modulestable (whose own caption asks to be kept in sync), the RCE3 six-summaries list, the OT2 "six real modules" figure and the step-13 barrel sentence all described a module set that had not existed for weeks: they named the deletedforms/and omittedconfigurationandpreferences. Six independent sources — directories, barrel exports, doc pages, API summaries, example folders and suites — agree on the same seven modules, so the set was never in doubt; only the prose was. The table is now the current census; the dated figures keep their historical counts and gain a clause naming today's.configurationalso gained the per-module checklist step it never had, having postdated OT2 and so reading as unvisited when in fact only its readme was missing. -
The refactoring guide briefed the next agent to do work that is already finished (tcinc1110).
_copilot_guidelines/refactoring_guide.mdcarried the RCF2 follow-up — remove the legacyforms/module and migratetom_uam_clientontotom_flutter_ui— as pending, and its per-moduleforms/entry said "scheduled for removal". Both halves have landed:lib/src/tomclient/forms/is gone andtom_uam_clientcarries thetom_flutter_uidependency the follow-up said it lacked. The two facts check each other rather than being read off a pubspec — the client analyzes clean while the module it used to import no longer exists, which is impossible if the migration were partial. A new dated RCF2 done-marker records the outcome and the per-module entry states it.The guide is a progress document, so being overtaken by events is its normal condition. It was worth fixing anyway because it is the package's own
_copilot_guidelines/file — the first thing an agent reads before touching this package. -
The guide's quality gate did not gate. Steps 12 and 13 and the per-module workflow told the agent to run
dart analyze. Measured in this package: with a single plantedunawaited_futuresviolation,flutter analyzeexits 1 anddart analyzeexits 0 — both print the diagnostic, but only one fails. Adart analyzegate passes a tree with lint violations in it. The live instructions now sayflutter analyze, with the measurement recorded beside them; the dated OT2 done-markers still say what OT2 ran. -
The reflection note pointed at a script this package does not have. "use the project's
reflection_generation.sh" — nine sibling packages undertom_ai/corecarry that script; this one does not. The note now gives the command that was actually run to verify it (dart run tom_reflection_generator, ~6 min) and names the three rootsbuild.yamlrestricts generation to, which is why the companions sit beside them rather than underlib/. Running it today rewrites all three (~400 k lines) because an upstream Flutter import dropped out and everyprefixNNalias shifts, so the note also warns against committing a regeneration as a side effect — the checked-in companions are the ones the green suite was measured against. -
Two documents said the
preferencesmodule had been deleted; it is the destination the removal record redirects to (tcinc1109). The status banner indoc/review_findings.mdand the per-module marker in_copilot_guidelines/refactoring_guide.mdboth recordedpreferences/among the four placeholders removed in 1.1.0. Both were true when written — 1.1.0 really did delete areadme.mdof research links from that directory — and neither recorded that the real device-preferences module was built in the same place three weeks later. Read today they say the package has no preferences module, contradicting the barrel, the source,doc/preferences.mdand the removal record, whose "where did local storage go?" answer is that module. Both now separate the placeholder from what replaced it.A third instance the filing did not name was fixed with them: the OT2 baseline blockquote in the same guide carries the identical claim.
Three further instances were found and deliberately left alone, which is the distinction worth recording. The 1.1.0 §Removed entry below, and the finding table and recommendation list in
review_findings.md, are accurate records of what a dated release did and what a dated review found. They differ from the three that were fixed not in wording but in role: a status banner and a per-module marker answer "how is it now", a changelog entry and a review finding answer "what happened then". Only the first kind can go stale.Nothing above introduces a count. The filing itself described the module as "seven files" when it is nine, which is the argument in miniature — an unchecked number in prose is a second copy of something that drifts.
Added #
-
security: the two panel-context fields nobody was watching are now pinned (tcinc691).
Tom2FAEnrolmentPanelContext.accountLabelis read — the TOTP enrolment panel captions the QR code with it — but_FakeControllernever overrode it, so it was''in every test and theisNotEmptybranch never ran. The documented behaviour (caption when named, omitted rather than blank when not) was asserted nowhere. Two tests now take both branches; a mutation forcing the guard either way kills exactly one of them.Tom2FAChallengePanelContext.hintwas worse: a search found the declaration, the default and the flow's one assignment, and no read at all. That is not a defect — TOTP is the only shipped mechanism and it has nothing to hint, since the user reads the code off the device in their hand. But it left the field's contract unverified, and the first mechanism to need a hint would have been the one to discover it never arrived. The framework's half of the contract is delivery, not display, and delivery is testable without a shipped reader: the test-only_SmsMechanismnow renders the hint, and two tests assert it arrives — populated when the server sent one, empty rather than null when it did not. The dartdoc,doc/security.mdand the API summary all now say outright that no shipped mechanism renders it, and why. -
security: the example demonstrates the flow instead of the annotation (tcinc690).
example/security/security_example.dartscoped itself to theTomClient2FAAdaptormarker — 41 lines about the one type in the module that the flow does not dispatch on — while the nine types the module exists for appeared in no example at all. A reader looking for "how do I use this" was answered with a question about an annotation.The example now implements a
Tom2FAFlowControllerand walks it through every stop of a second-factor login: the chooser, the enrolment, a refusal and itsdescribeErrorrendering, the confirmation, and a challenge on the next sign-in with its attempt budget counting down. It also shows the registry rule that decides what the chooser may list, and the deliberately conspicuous missing-key behaviour ofTom2FATexts.A controller is what the tests cannot show: the one in
two_factor_client_test.dartis a stub whose moves record their argument and never advance the login, because a widget test wants each stop pinned rather than reached. Conversely the panels are widgets and need a pump loop no example can open, so the header points at that suite for them.run()is nowFuture<void>, andtest/security_test.dartawaits it. -
security: the chooser renders the mechanism's icon (tcinc689).
Tom2FAClientMechanism.descriptorwas documented as carrying "its label, description and icon, all as resource keys", but_methodTilebuilt itsTomListTilefrom the first two only, andiconKey— populated for every mechanism byTom2FAMethodDescriptor.scoped— was dead data on this tier.The chooser now fills
TomListTile.leading. Resolution is the two-step conventiontom_flutter_uialready uses everywhere else: the text bundle mapsiconKeyto an icon name (mSecurity), and the application'sTomUIIconsadapter maps that name to anIconData. No new resource pipeline, and no new dependency — the widget library was already imported.The icon is an enhancement, never a requirement. A bundle with no icon entry, an application that installs no adapter, or an adapter that does not carry that icon each render the row without a leading icon and lose nothing else. A missing key cannot leak the way missing text does, either: an unknown key resolves to the key itself, which is conspicuous on a label by design, but
two_factor.sms.iconis not an icon name, so it resolves to nothing. -
runtime: the per-read allocation of
clientPlatformUtilsis pinned by test (tcinc683). The getter constructs a new instance on every read, both documents say so, and until now nothing observed it — the suite read the getter fourteen times and never once looked at what came back. Three tests now state the contract: two reads are distinct objects; a read taken aftersetCurrentPlatformis not the registered instance, so comparing platform-utils identities is wrong today; and a second read re-imports the process environment, which is the work the repeated construction actually costs.The environment merge is idempotent against an untouched map, so the only way to watch it happen is to overwrite one key and see a construction repair it. That test carries a control assertion for the same reason:
getTomEnvVars()returns the shared static map rather than a copy, and if it ever stopped doing so the overwrite would land nowhere and the check would pass having observed nothing.These tests take no position on whether the getter should allocate per read — that question is open, and it is about exactly this cost. They record what the code does now, so that changing it has to be a decision rather than a side effect, and so the answer arrives with a test saying which way it went.
Changed #
-
runtime: the neutral fallback's
httpClient()throwsUnsupportedErrorrather thanUnimplementedError(tcinc682). The two types say opposite things about the same situation.UnimplementedErrormeans "planned, not yet written", and reads as an invitation to finish something — but the neutral variant is the deliberate fallback for a target that exposes neitherdart:ionordart:js, and on such a target there is noClientimplementation to construct, ever. The condition is permanent, which is whatUnsupportedErrorexists to express.The message was also rewritten. It was ungrammatical ("You need initialize"), and it instructed the reader rather than naming the fault — so a caller reading it out of a crash log learned what to do but never what had happened. It now says the neutral fallback has no HTTP client, that the target exposes neither
dart:ionordart:js, and only then what to do about it.doc/api_reference/api_summary_runtime.mdhad faithfully mirrored the wrong type;doc/runtime.mdsaid only "throws", which was never false but gave a caller nothing to catch on. Both now nameUnsupportedError, and a test recomputes the name from the throw rather than restating it.The test asserting the type has to exclude the subtype explicitly, because
dart:coredeclaresUnimplementedError implements UnsupportedError: a test checking onlyisA<UnsupportedError>()would have passed on the old code and proved nothing. That trap is recorded indoc/runtime.mdbeside the behaviour it guards.The tests run against a real instance, not against the source text. The neutral variant imports only Flutter,
httpand the kernel, so it is constructible on the VM host — as isflutter_io. Two of the three variants are directly exercisable from one host; onlywebis not, becausepackage:webneeds JS interop.doc/runtime.mdhad said the variants "cannot all be exercised from one host", which is true only of all three at once and had been read as though none could be; it now says which.
Removed #
-
runtime: the six
out/outErroroverrides that restated their inherited bodies (tcinc681). Each ofplatform_detection_flutter_io.dart,platform_detection_neutral.dartandplatform_detection_web.dartdeclaredvoid out(String s) => print(s);andvoid outError(String s) => print(s);, which is exactly whatTomFallbackPlatformUtils— the base all three extend — already does. Deleting them changes nothing at runtime and removes the standing risk that a future change to the base's console routing is silently overridden back toprinton every Flutter target. Each variant now overrides thirteen members, nine of them identical in all three; the API summary marks the pair as inherited rather than as overrides.The duplication between the variants is forced — they are compile-time selected, so a shared body has nowhere to live but a base class in another package — and it is what the standing "extract a shared base" question is about. Duplication against the base is a separate thing and needed no such extraction; the previous entry recorded that and this removes it.
A guard now compares every variant override against the body it inherits, reading the base out of
tom_basicsthrough the package config. It reduces both sides to the expression they evaluate first, because the two are written in different forms —=> print(s);in the variants against{ print(s); }in the base — and a comparison of the text as written would have reported all six as different and found nothing. The member-count check was widened to pin the total as well as the duplicate count, since deleting a member moves both and checking one leaves the other free to keep a stale figure.
Fixed #
-
security: the QR view no longer re-encodes and repaints on every parent rebuild (tcinc688).
_QrPainter.shouldRepaintopened withold.image != image.QrImagedeclares nooperator ==, so that was an identity test — andTomQrCodeView.buildallocated a freshQrImageevery time it ran, so the clause was true on every rebuild and the guard could never returnfalse. The remaining clauses were unreachable.The cost was not theoretical: a 114-byte enrolment URI encodes in 2.6 ms on a warm JIT on an M-series Mac — about 16 % of a 60 fps frame budget — because
QrImagetrials all eight mask patterns and scores each. The enrolment panel rebuilds on every busy flag and every controller notification, and paid that on each one, plus a full module repaint.TomQrCodeViewis now aStatefulWidgetthat encodes ininitStateand re-encodes indidUpdateWidgetonly whendatachanged;size,paddingand the two colours are paint-time inputs and deliberately do not trigger a re-encode. The painter holds the payload and compares that rather than the image, so the guard stays correct independently of how the encoded value is cached. -
security: an
otpauthUritoo long to encode blanks the QR code instead of throwing out of the enrolment panel (tcinc687).TomQrCodeView.buildguarded an empty payload and then encoded an unguarded one. A QR code has a hard capacity ceiling — 2331 bytes at the medium correction level the widget uses — and past it theqrencoder throwsInputTooLongExceptionfromQrImage's construction, which is insidebuild. The result was a crashed enrolment screen.Reachable from wire data along the ordinary path, not only by driving the widget directly: an
otpauth://URI grows with the issuer and account label a deployment configures, and nothing between that configuration and the widget bounds their length. ATom2FAFlowPanelpumped over an enrolment whoseotpauthUriwas oversized threwInput too long. 48396 > 18672out of build.This is the exact failure the module's own defensive-reading contract says it prevents — but that contract only ever covered missing keys, not values that arrive and cannot be used. An unencodable payload now renders the same empty square an empty one does, which is safe here specifically because the enrolment panel shows the base32 secret beside the code: the user keeps a complete enrolment path. The encoder exposes no capacity predicate, so the case is caught rather than pre-checked, naming the one exception type that means "too long" so a genuine encoder fault still surfaces as a crash.
TomQrCodeViewnow also holds its requested size when it renders nothing, so a payload that crosses the ceiling between two enrolments does not shift the layout around it. Four tests were added: the panel-level case beside the existing missing-URI one, and three driving the widget directly, since it is exported and this is now a promise to every caller. -
security: the TOTP
clientDatakey list namesperiodSeconds, the fourth key the server sends (tcinc686).TomTotp2FAClientMechanism's dartdoc said it read "exactly the keys the server'sTomTotp2FAServicewrites, and nothing more", and then listed three. The server writes four:secret,otpauthUri,digitsandperiodSecondson an enrolment offer,digitsandperiodSecondson a challenge. The claim was false in both directions at once — the server writes a key the table omitted, and the mechanism reads nothing of it.The same omission stood in
Tom2FAChallengePanelContext.clientData's dartdoc ("For TOTP:digits"), which disagreed with its own siblingTom2FAEnrolmentPanelContext, whose dartdoc named all four for the same wire payload.doc/security.mdanddoc/api_reference/api_summary_security.mdcarried the three-key list too.This was not cosmetic.
periodSecondsis the value a validity countdown would be driven from, and its absence from every documented key list is what made the missing countdown look like a change that needs new wire data. The data is already arriving on every challenge. All four documents now list the key, distinguish written-by-the-server from read-by-the-panels, and say plainly that the shipped panels discard it. No constant was added for it, because nothing reads it yet. -
security: the module's documents describe the second-factor feature rather than only its annotation (tcinc685). All three consumer-facing documents —
doc/security.md,doc/api_reference/api_summary_security.mdand the module readme — stated that the module contains one marker annotation. It contains six files and ten public types, every one exported from the barrel, and the nine they omitted are the feature: the flow panel and its three stops, the controller seam, the mechanism contract and its registry, the two panel contexts, the shipped TOTP mechanism, the sixteen resource keys and the QR view. The API summary additionally named one of the six source files as the source, anddoc/security.mdlisted onlytom_core_kernelas a dependency where the module also reaches forflutter,tom_flutter_uiandqr.The cost was not cosmetic. A developer opening
doc/security.mdto find out whether the framework offers a second-factor UI was told, in three places, that it does not — so the feature was reachable only by reading the source or already knowing it existed. All three are rewritten from the source, and the package README's two descriptions of the module with them.Stating the module from its source is also what surfaced the defect the
Removedentry above records: the documents had described the marker annotation as the module's dispatch mechanism, and writing down what the code actually did made it plain that nothing read it. -
runtime: the example prints the Flutter target beside the predicates it explains (tcinc684). Run as its header prescribes, the example printed
isDesktop=false isMobile=trueon a macOS host and accounted for it nowhere — a module whose one job is platform detection appearing to get the platform wrong. It was not wrong: every predicate readsdefaultTargetPlatform, which underflutter testisTargetPlatform.androidwhatever the host, so the output was correct about the target and silent about the difference between that and the machine. The example now printsflutterTarget=…immediately above the predicate line, so the answer arrives with its cause and stays true under any harness rather than carrying a frozen remark about android.The assertions were re-framed for the same reason. Their comment introduced the whole block as following from "running under the VM", which is true of
isWeb()and the browser location but not of the desktop/mobile split — that follows from the target. One assertion also could not hold in general:isDesktop() || isMobile()is false on Fuchsia, which is in neither family. It is replaced by one that ties the mobile answer to the target explicitly, which is the claim the example is actually making.httpClient()is now demonstrated too. It is one of the few members that genuinely differ per variant and the onedoc/runtime.mdopens with, yet the one executable artifact that would break if it stopped working never called it. The example takes a client, prints its runtime type and closes it.Three tests read what the example printed rather than what its source says, since the output is the part a reader sees and the only part that survives into a bug report: that it names the effective target, that the family it reports agrees with that target, and that it exercises the HTTP client. The target is recomputed in the test from
defaultTargetPlatform, so the guard keeps working wherever the suite runs.Both documents gained the caveat:
doc/runtime.mdas a note on what the predicates measure and what that means for tests (pin the target withdebugDefaultTargetPlatformOverride) and for diagnostics, and the API summary as a line above the methods table, where every "True on …" needed saying that it means the target. -
runtime: the inventory of what the three variants share is counted from the sources (tcinc680). Three documents described one set of files and no two agreed.
doc/runtime.mdsaid the variants "differ only in construction,httpClient(),getBrowserLocation(), andgetIsolateName()" — omittingtoString(), which differs in all three and is the module's diagnostic surface, so precisely the wrong omission. The API summary undercounted further, naming only the HTTP client and the browser location. The quest completion note counted "nine predicates" copied across "the web and neutral variants", missingoutandoutErrorand describing two files where the duplication spans three.Counted mechanically instead: each variant overrides fifteen members, eleven are identical in all three — the nine predicates plus
outandoutError— and four differ. All three documents now say that, and note thatgetBrowserLocation()is the weakest of the four differences, since native and neutral both returnnull.None of this changes behaviour, but all of it feeds one decision — whether to extract a shared base — and an inventory that undercounts the duplication understates the case for it.
outandoutErrorturn out not to need that extraction at all: both reproduce the inheritedTomFallbackPlatformUtilsbodies exactly, so all six overrides are redundant and removable on their own.Three tests hold it, and they recompute the partition from the three sources rather than restating a number: one asserts each document's list of differences is exactly the differing set (word-bounded, so a short name like
outcannot match inside another word); one asserts each document spells the computed count of duplicated members; one pinsout/outErrorto the inherited body, so the redundancy claim fails loudly if either stops beingprint(s). A hand-maintained census of a set the compiler can enumerate is going to drift — this one drifted three ways at once. -
runtime: the documents agree on what the barrel actually exports (tcinc679).
doc/runtime.mdsaidallowsBadCertificateForHostwas "not part of the barrel's public API"; the API summary listed it under Global Functions and Constants, besideclientPlatformUtils, with nothing to tell the two apart. A reader following the summary would import the barrel and get an undefined-function error.The summary was the document in the wrong. The barrel exports one file,
platform_detection_client.dart, and that file reaches the per-target variants with a conditionalimportrather than anexport— so nothing from a variant escapessrc/. Verified by compiling a probe against the barrel alone, which also showed the gap was wider than the two-document disagreement suggested:TomFlutterPlatformUtilsis unreachable too, and a third document not implicated in the discrepancy —api_summary_index.md— tabulated both withheld symbols alongside the exported one.Neither symbol can be fixed by exporting it. A conditional export must name a symbol present on every target, and only the native variant has a TLS callback to configure;
TomFlutterPlatformUtilsis three unrelated classes sharing a name, so it denotes no single exportable type. Both are therefore documented as internal, under an Internal Symbols (not exported) heading, with thesrc/import a test needs — and a new Exported Surface section states the one-symbol rule and why it is not an oversight.Three tests hold it: one asserts the entry point contains no
exportdirective and the barrel never names a variant file, which is the structural fact the whole claim rests on; one asserts the summary quotes the import path this suite itself compiles against, read out of the suite rather than hard-coded, so a file move cannot leave a stale path documented; and one scans the enumerating lines of both API documents — headings, list items, table rows — for a withheld symbol presented as exported. Scoping that last scan to enumerations rather than to whole paragraphs is deliberate: prose legitimately names the variant class while explaining how one gets selected, and it was the enumerations, not the prose, that misled. -
runtime: the platform contract is documented as a
tom_basicstype, not a kernel one (tcinc678).TomPlatformUtilsandTomFallbackPlatformUtilsare declared intom_basics; this module can reach them only becausetom_core_kernel.dartre-exports the whole of that package. Every import line here therefore names the kernel, and the documents had followed the import rather than the declaration — the API summary went as far as annotating the base class "(kernel)" in its hierarchy diagram. A reader who went totom_core_kernelfor the contract's dartdoc, or to see what a new predicate would have to override, found nothing there.Corrected in nine places across three documents and one library dartdoc — the claim had spread well beyond the two lines it was first noticed on. The Dependencies section now distinguishes the two edges instead of swapping one package name for the other:
tom_core_kerneldirect and re-exporting,tom_basicstransitive and declaring. The README's architecture diagram grew atom_basicslayer beneath the kernel, which is also where the layering it draws actually stops. The same fix covers the env-var map, documented as the kernel's for the same reason and declared in the sametom_basicsfile.Two tests hold it: one reads the package config to assert the contract really is declared in
tom_basicsand nowhere undertom_core_kernel/lib, and one scans the module's documents for a line that names the type and calls it the kernel's without saying the kernel is only the re-exporting barrel. Prose review had not caught nine restatements of one wrong fact; a scan does. -
runtime: the API summary no longer splits
outandoutError(tcinc677). The summary describedoutas printing to standard output andoutErroras printing "as an error", a routing distinction that exists nowhere in the hierarchy: both methods areprint(s)in all three variants and in theTomFallbackPlatformUtilsbase they inherit from. A reader who trusted the summary would send diagnostics tooutErrorexpecting stderr and find them interleaved with normal output on stdout. The row now says what the code does — same destination asout, named apart but not routed apart. Whether the two ought to be separated is atom_basicsdesign question, untouched here.The test that let this stand asserted only
returnsNormally, which cannot see a destination. It is replaced by a print-zone capture that asserts what each method writes, and by a test that reads the two method bodies out of the IO variant and holds the summary to describing them the same way — so if the two are ever routed apart, the source expectation fails and asks for the summary to be revised rather than leaving a sentence that has quietly stopped being true. -
runtime:
toString()spells the class it names (tcinc676). All three compile-time variants renderedTomFlutterPlatformUtis, dropping thel, while the class isTomFlutterPlatformUtilsin each.toString()is this module's diagnostic surface — it is what an application logs to record which variant it selected, and what the module's own example prints twice — so anyone grepping logs for the class matched the source and missed every runtime record of it. The IO variant now readsTomFlutterPlatformUtils: io, and web and neutral likewise.Two tests replace the substring assertion that let this stand for so long: the reachable variant is pinned to its whole string, and a source-level test requires each of the three variants to open its rendering with the class it declares. Only the IO variant can be constructed on any one host — the other two are selected by conditional import — so without reading their source nothing pins them at all.
Added #
-
resources: the example demonstrates both construction-time failures (tcinc675). It showed three ways resolution succeeds and no way it fails, though failing is half of this module's contract and the harder half to discover — both failures happen while the property is being declared, not when it is read, so a reader who has only seen the success path has no reason to expect a
tryaround a field initialiser. Two sections were added: an obligatory key that is not configured, and a value that cannot be cast to the property's type. Each catchesTomPropertyException, prints it, and asserts the matchablekeyplus that the message names the failing config key; the type-mismatch one also asserts thatrootExceptioncarries theTypeError, which is the only record of the value's actual runtime type. The mismatch is shown through.withDefaulton purpose — a default covers an absent key, not an unusable one, so it does not soften a mistyped entry. -
resources: the suite pins dot-notation resolution, the null-value collapse on both branches, and
~across all four subclasses (tcinc674). Three documented behaviours had no test. Every config in the file was written as a flat map under a literal dotted key, which the provider answers by plain map lookup — so nothing exercised the nested tree walk that a real configuration file actually takes, even though dot notation over nested maps is the module's advertised resolution rule. Four tests now cover it: all four typed subclasses reading a nested leaf, an interior node yielding its subtree whole, a half-written path reported as an ordinary miss, and — new to the documents as well as to the suite — the precedence between the two spellings. The walk runs first and the flat lookup is its fallback, so a config holding both{'app': {'title': …}}and{'app.title': …}answers with the nested one. The opposite order is an equally plausible reading of "either a flat entry or a nested one", which is why it needed pinning rather than assuming.The second gap was half-covered: a written
nullreporting as absent was asserted for the obligatory branch only. It is now asserted for the default branch too, flat and nested, which is where the consequence is sharpest — a caller can writenullas a deliberate default but never as a deliberate value, so an explicitnullin a config file can never override a non-null default whatever its author meant by writing it. A third test pins the recourse the documents now promise for callers who must tell "explicitly unset" from "never configured": the provider'sresourcesmap is public andcontainsKeydoes distinguish them. Making that map private or lazy would break the promise silently while every other test still passed.The
~operator was asserted onTomStringPropertyalone, while the deliberate all-four-subclasses sweeps stopped atget()— an omission that read as an oversight rather than a judgement. Both sweeps now read each value twice. The operator is declared once on the generic, so what this pins is not four implementations but that all four subclasses still inherit the one, and that it answers on the fallback branch where there is no configured value for it to return.
Fixed #
-
resources: the test comments state the module's contract instead of citing a document that was never committed (tcinc672). Four comments in
test/resources_test.dartdocumented the module's history rather than its behaviour, and the history was unreachable. Three opened with a citation —tests §9(a),§9(b),status_bug.md §17— naming result documents from a code-review plan (§8of that plan listsstatus_bug.mdamong the files a review was to produce). They were never produced: nothing in the workspace carries that name, so every one of those references led nowhere. Two closed with "RED before the fix", a marker true only for as long as it took to make the test pass.The narration had also gone stale in a second way. One comment described falling through a
_defaultValue != nullguard that the module no longer has — default presence is tracked separately precisely so that branch cannot occur — and a fourth namedTomConfigResourceProvider.existsas the reason a written null reads as absent, which stopped being the mechanism when resolution moved togetObjectOrNull. All four now state what the test pins: what the type-mismatch report has to name, why an absent key without a default is a failure rather than a null, which lookup makes a written null and an unwritten key indistinguishable, and why an explicit null default is honoured. -
resources: property resolution reads the configuration once, reports failures as
TomPropertyException, and no longer claims failures it did not diagnose (tcinc671).TomProperty._getValuehad three defects in six lines. It asked the store the same question twice —exists, thengetText— and then a third time inside the catch block, to interpolate the value it had just been handed, on the one path whose premise is that the store had already answered. It bound the caught error and never referenced it, so whatever the runtime actually objected to was destroyed and the replacement carried no root cause. And its catch was unqualified while its message asserted a cast failure, so any other failure in the lookup path would have been reported as "could not set property of type T from value …", sending the reader to audit type declarations for a fault in the config source.Resolution now uses the kernel's
getObjectOrNull, which answers presence and value in one lookup — and which the kernel's own dartdoc already recommended over theexists-plus-getTextpair for exactly this reason. The read sits outside thetry, which holds nothing but the cast; the catch ison TypeError; and the message interpolates the local.nullfrom that single lookup means absent, which preserves the existing behaviour that a key configured with no value does not satisfy an obligatory property.Both failures now raise
TomPropertyException, aTomClientExceptiondeclared in the module, with matchable codesresources.property.not_configuredandresources.property.type_mismatch,parameterscarrying the key (plusexpectedTypeandvalueon the mismatch path), androotExceptioncarrying theTypeError— the only record of the value's actual type, since the message names only the type that was wanted. One type with two codes rather than two types, following the kernel's own resources module.doc/resources.md,doc/api_reference/api_summary_resources.mdand the module readme previously described these as a bareExceptionwith no code; all three now describe what is thrown, and the API summary's Exceptions section — added in tcinc670 when the failures were still untyped — gains the class, the codes, the payload and the rule that a read failure is never rewritten into a cast failure. -
preferences:
TomStoredDevicePreferencesno longer modifies the list its store handed back (tcinc667).setassigned into or appended to the list returned byTomDevicePreferenceStore.read(), andremovecalledremoveAton it. The three shipped backends survived that only incidentally — the memory one copies defensively, the file and web ones decode a fresh list per call — and nothing required either property, so the module worked by luck of its own implementations rather than by its contract. A conforming backend that returns its own list, or an unmodifiable view of one (the natural shape for a cache overshared_preferencesor a keychain), failed instead: everysetthrew a rawUnsupportedErrorthat no caller could map to a preference failure, andremoveansweredfalsefor a key that was present. Both now copy before mutating —removeonly on the path that actually writes, so a miss still allocates nothing. The asymmetry that made this visible is gone too:all()already returnedList.unmodifiable(...), so the module demanded of its backends exactly what it refused to promise its callers. -
preferences:
TomPlatformDevicePreferenceStore's constructor is no longerconston any variant (tcinc665). The same defect astcopen198below, in the module with three variants instead of two: thedart:ioand web implementations declaredconstconstructors, and the neutral one cannot, because throwingdevice_preferences.unsupported_platformfrom its body is its entire purpose. Soconst TomPlatformDevicePreferenceStore(location)compiled on device and in the browser and broke the build on the one target whose intended behaviour was a run-time failure — converting a locatable startup diagnostic into a compile error at an unrelated call site, on the platform where the developer has the least context. That also contradicted the module's own promise that the store "takes onelocationon every platform … so the construction above is written once and never branches". Theconstis dropped from both variants that had it, each keeping a dartdoc paragraph recording why so it is not "restored" later, anddoc/api_reference/api_summary_preferences.mdnow states the signature that is true everywhere. No call site anywhere in the package, its examples or its documentation wroteconst, so nothing downstream changes. -
configuration:
TomFileClientConfigurationStore's constructor is no longerconst(tcopen198). The store is chosen by conditional export, and the two variants disagreed: thedart:iovariant declared aconstconstructor while the neutral one cannot — its whole purpose is to throw from its body. Soconst TomFileClientConfigurationStore(path)was legal Dart on desktop and mobile and a compile error on web, and the compiler only said so once someone built for the platform they were not working on. A conditional export is worth using only if one name means one signature on every target, so theconstis dropped from the io variant; the constructor now carries a dartdoc paragraph recording why, so it is not "restored" later. The base class keepsconst TomClientConfigurationStore()— it permitsconstimplementations, it does not require them. -
configuration:
save()carries forward from the live store, not from a stale snapshot (tcinc649).TomBaseClientConfiguration.save()seeded its carry-forward — the keys the running build does not declare, which must survive a write — from the in-memory_persistedsnapshot taken atload(). That snapshot describes one store at one moment, and the two ways it diverges from reality both broke the promise it was meant to keep. A holder that loaded from store A and was then pointed at store B deposited A's foreign keys into B; and a key written by anyone else sinceload()was absent from the snapshot, so it was deleted rather than carried forward — precisely the loss the mechanism exists to prevent.save()now re-reads the store it is about to write. Thestoresetter additionally discards the snapshot on a real change (re-assigning the same store is a no-op), so a setting declared after a swap no longer picks up the old store's override; the settings themselves keep their values, because a swap is not a load. This is the fix recommended undertcopen200; the residual, now documented under Limitations, is that read-then-write is not atomic, so two genuinely simultaneous saves can still lose one another's last write undetected — the exposure is one store round-trip rather than the whole session.
Documentation #
-
resources: the API summary documents what the module throws (tcinc670). The summary was symbol-accurate and silent on failure, which is an expensive omission in this module specifically: both constructors resolve in their body, so the two throw sites are not error paths reached after a successful construction — they are the two ways construction fails, at whatever point in start-up the property is built. The constructor table said "throws if absent" and nothing about the type-mismatch path, so a reader got neither the exception type, nor the message shape, nor the fact that a wrong-typed present value fails as hard as a missing one. That also made
resourcesthe exception to the package's own pattern: of the eight API summaries, the three whose modules throw all document it. A new## Exceptionssection now gives both failures with the message each produces verbatim, states that they are bareExceptions carrying no stable code and are therefore distinguishable only bytoString(), and records that "absent" includes a key whose configured value isnull. The constructor table now says resolution happens during construction, and thatwithDefaultstill throws on a wrong-typed present value — the default covers an absent key, not an unusable one. -
resources: the recommendation to prefer a typed subclass now states the one case where it does not apply (tcinc669). The class dartdoc, the module readme's key-types table,
doc/resources.md's overview table and the API summary's Overview each told the reader to use the typed subclasses rather than the generic, and none of the four admitted an exception. One exists, and it is not marginal:TomStringProperty,TomIntProperty,TomDoublePropertyandTomBoolPropertyclose over non-nullableString,int,doubleandbool, so an optional value — thewithDefault(key, null)shape — cannot be written with any of them. The only form that expresses it isTomProperty<String?>.withDefault('app.region', null), the very construction all four documents forbade, and the one the module's own suite has exercised since the explicit-nullfix. A reader who needed an optional property was therefore told their sole correct option was against the module's contract, and either reached for it uneasily or wrote a sentinel default and a test for it. All four now scope the recommendation to the four non-nullable value types and name the nullable-Texception, and the three consumer-facing documents print the construction beside it. -
resources: the documented dependency is the provider the module actually reads from (tcinc668).
doc/resources.mdlistedTomConfigResourceProvider/TomTextResourceProvideras "the resource providers the property reads from". Only the first is true: both ofTomProperty's hooks resolve throughTomConfigResourceProvider .getAppConfig(), and the text provider is not referenced anywhere in the module. The line was left behind when the module's general-purposet()translation lookup was removed, and it was not harmless — the two are separate provider families with separate installation calls (setAppConfigProvidervssetAppResourceProvider) over separate backing maps, so a reader who followed it installed the text provider and found that no property resolved at all. The section now names the config provider alone and says which call populates it, and states explicitly that installing the text family instead leaves every property unresolved. -
preferences: the store contract says who owns each list (tcinc667).
TomDevicePreferenceStoreis the whole backend contract — two methods — and its dartdoc said whatread()returns without saying whether the caller may modify it, or whatwrite()may do with what it is given. Both halves are now stated, on the methods themselves and indoc/preferences.md, which gains a Writing a Backend section for the reader the contract is actually addressed to: the listread()returns belongs to the store, so an implementation may hand out its own list, an unmodifiable view, or the same instance every time and needs no defensive copy per read; the list given towrite()belongs to the caller, so an implementation must neither modify it nor retain it without copying —clear()passesconst [], which no store could keep. The rule is repeated in the API summary's method table and as a clause in the module readme's type table, and a test now pins it in all four artefacts, so the sentences stay rewritable but the fact cannot quietly disappear from any of them. -
preferences: the scope rule reaches the documents a reader actually starts from (tcinc666). A device store has one user because its location has one user — not because anything here tracks one. That rule, the per-account
locationpattern and the shared-install failure mode were stated in the library dartdoc and the modulereadme.md, and in neither of the two consumer documents. Following the package documentation rather than the source tree therefore led to a multi-account application built on a single store, whose second sign-in reads the first user's last-used account, biometric flag and layout — with no warning, no log line and no assertion, because a store is never told a user changed.doc/preferences.mdnow carries a Scope section stating all three, andapi_summary_preferences.mdnames the consequence on thelocationentry itself. The forward reference is placed on thegetForparagraph specifically: "a device store has one user by definition" is where the misreading forms, because it does not say what makes the definition true. The section also maps the choice onto the configuration scopes — fixed location is CE-CC(client app, machine), per-account is CE-DS(user, device), same class at a different address. -
Four API summaries list their last section in their tables of contents (tcinc666).
api_summary_little_things.md,api_summary_observing.md,api_summary_resources.mdandapi_summary_security.mdeach ended with a## Global Functions and Constantssection that their contents block did not mention, so the one place a reader looks to find out whether a module has top-level API said nothing either way. Found by the new structural guard rather than by reading, which is the point of having it. -
preferences / configuration: the exception constructor blocks show
autoLog's inherited default, and say where their types come from (tcinc664). Both preferences blocks documentedbool autoLogwith the= falsedefault dropped, andapi_summary_configuration.mddocumentedbool? autoLog, which no call site can pass —autoLogis non-nullable, so omitting it and passingnullare not the same thing and the second does not compile. All three now showbool autoLog = false.The
stackparameter in the same blocks was not wrong and is unchanged.StackTrace? stackhas now been reported as a mistype three times (tcinc617 for the server's summary, tcinc654 forlittle_things, tcinc664 here), always on the same reasoning: the field onTomBaseExceptionisObject?, so the parameter "should" be too. It should not. These classes declare no types of their own — every parameter arrives bysuper.x, and a super-parameter resolves against the immediate superclass constructor.TomException's constructor declaresStackTrace? stackoutright and forwardsstack ?? StackTrace.current, soStackTrace?is what a caller may pass; documenting the parameter from the field declaration is the plausible-looking wrong answer, which is why it keeps being filed. Both summaries now carry the paragraph that says so, joining the oneapi_summary_little_things.mdalready had. -
observing:
ValueListenableObserver's contract deviation is documented at both ends, not one (tcinc662). The adaptor departs from theValueListenablecontract in two independent ways —addListenerreplaces the single slot instead of appending, andremoveListenerignores its argument and clears the slot regardless. Every consumer-facing artefact stated the first and none stated the second:doc/observing.md, the API summary's callout and itsremoveListenerrow ("Clears the registered listener" — wording a reader takes to mean that listener), the class dartdoc and the module readme. The asymmetry left the module's own example unexplainable, in that it clears the adaptor by passing a closure it never registered, and a half-stated deviation is worse than an unstated one because the half that is written implies the other half behaves normally. All five now state both halves and say why — there is one slot rather than a list, so there is nothing to match the argument against. -
observing: the weak-reference contract is stated the right way round (tcinc661).
doc/observing.mdclosed its lifecycle section by claiming that because theStatederegisters indispose, "a long-lived observable that outlives the widget is therefore not retained" — an inversion in both directions it could be read.TomObservableholds its observers byWeakReferenceand prunes dead ones on notification, so the registration never kept theStatealive and removing it is not what releases it; and the strong reference runs the other way, throughTomObservingWidget.subState, so the widget keeps the observable alive anddisposedoes nothing about that either. The claim had been copied into the widget's dartdoc anddoc/api_reference/api_summary_observing.md, and its inverse was even written into the comment on the test that pins the correct half, so all four are corrected together: the section now names which side holds which, links to the kernel's own memory-management rule, and says what deregistration actually buys — a known moment for notifications to stop, rather than leak prevention. The corollary is the part worth carrying away: an observer that is still reachable but no longer wanted keeps firing for as long as something else holds it, so collection is not a substitute for removing it. -
configuration: validation is described instead of deflected (tcinc651). Both the
TomBaseClientConfigurationdartdoc anddoc/configuration.mdsaid per-field validation was "not the holder's job" because "constraints ride on the field declaration (theCs*annotations)".CsClientConfig— the marker for this exact scope — carrieskey,envAlias,overridableByandnote, and no constraint of any kind; noCs*annotation anywhere carries a range, a pattern or an enum.tom_code_specsis also not a dependency of this package, so nothing here could reference them even if they did, and neither artefact said whereCs*lived. The sentence sent the reader somewhere that does not have what it promised. Both now state what is true: the type is checked (client_configuration.value.type_mismatch) and the value never is, aTomSetting<int>stores and serves-1as a refresh interval, there is no hook to add a check becauseTomSetting's constructor is private, and range, format and enum constraints are the caller's.doc/configuration.mdalso gained the matching Limitations bullet — the section is the document's own inventory of gaps, and a reader who scans it was previously left to conclude that validation was covered. No API change. -
configuration API summary completed and guarded (tcinc650). The mirror at
doc/api_reference/api_summary_configuration.mdwas accurate in everything it listed but silently omitted six public members the sources declare:TomSetting.toString(),TomBaseClientConfiguration.toString(),TomClientConfigurationStore'sconstconstructor (with its "allowsconstimplementations" note),TomFileClientConfigurationStore.path, that store's non-constconstructor rationale, and — found while verifying rather than trusting the mirror —TomClientConfigurationException's constructor, which the sibling summaries document under#### Constructors. All six are now listed. The completion document's class sketch was corrected to match the source's non-constconstructor, so the two mirrors agree. -
configuration: the completion document's provenance claim is true again (tcinc652). Its "Using it" part opened by saying everything below came from
example/configuration/configuration_example.dartandtest/configuration_test.dart; three of its twelve subsections were in neither. Rather than narrowing the sentence, two were made real — theTomObservingWidgetsnippet and theunsupported_platformfailure are now executed by the suite (see below) — and the third, thegetApplicationSupportDirectory()path expression, is marked in place as the document's single unexecuted line, because quoting it truthfully would mean taking apath_providerdependency the package does not otherwise need and a doc snippet is not a reason to acquire one. The provenance sentence now also records that the suite runs the example, so these are executed snippets rather than transcribed ones. No API change. -
little_things: the API summary types
autoLogcorrectly, and the hierarchy goes as deep as the classes do (tcinc654). Both constructor blocks indoc/api_reference/api_summary_little_things.mddeclaredbool? autoLog, which no call site can satisfy — the kernel'sTomExceptiondeclaresthis.autoLog = false, so passingnullis a compile error and the default is inherited through the super-parameter chain. Both now readbool autoLog = false. Thestackparameter, filed alongside it as an identical error, turned out to be right as it stood: asuper.xparameter takes its type from the immediate superclass constructor, and althoughTomBaseExceptiondeclares the fieldObject?,TomException's constructor already narrows it toStackTrace?— soStackTrace?is what a client call site may pass, and changing it toObject?would have introduced the error the report set out to remove. The summary now states that rule where the two types meet, rather than leaving the next reader to re-derive it. The class hierarchy, which stopped atTomException (kernel), namesTomBaseException (tom_basics)above it — most of the inherited payload is declared there — and the Overview records the three kernel arguments (requestUuid,uuid,serverCallError) the client constructors do not surface. The completion document's payload table typesstackasObject?and stays that way: it tabulates fields, and the field really isObject?; it now says so explicitly so the two artefacts are not read as contradicting each other again. No API change. -
little_things: the prose document's one outbound link goes where it says (tcinc656).
doc/little_things.mdrendered the base type as[TomException](../../tom_core_kernel/doc/security/security.md). The path resolved, so nothing flagged it — but the target contains the stringTomExceptionzero times, and the link sits on the sentence enumerating the payload the client types inherit (key,defaultUserMessage,parameters,stack,rootException,autoLog), so a reader who followed it to learn what those mean arrived in the authorization documentation. It now points at../../tom_core_kernel/doc/little_things/little_things.md, which has a### TomExceptionsection and covers every one of the six terms. A sweep of all 48 repository-relative links in the package found this to be the only wrong target and, in_copilot_guidelines/index.md, two dead ones — the sibling-package links../tom_core_kernel/and../tom_core_server/were one level short of the sibling directory and are now../../.
Tests / Examples #
-
the test-comment guard is now a package-wide suite rather than a group inside the file it policed (tcinc672).
test/test_comment_hygiene_test.dartreadsobserving_test.dart,resources_test.dartand its own source, and fails on a comment that records the phase of its own authorship or cites a numbered section of another document. It is deliberately scoped to that list rather than banning the section sign package-wide:codespecs_mapping.mdand the completion corpus's contract are cited by number elsewhere and both exist.The relocation is what makes the positive half of the guard work. Both negative checks pass against a file whose comments have simply been deleted, so each guarded file must also keep making the claims its comments exist to make. While the guard lived inside
observing_test.dart, a needle written there was also a needle present in the file being read, and the check passed against its own argument — it stayed green with the comment it guarded removed. Reading a file other than the one the needles live in fixes that structurally. Two vacuity pins keep the three searches honest, since each of them passes by finding nothing: one requires every scanned path to exist and be non-empty, the other that no required claim is registered against a file outside the scanned list. Seven mutations, each firing its intended test and no other: deleting a guarded comment, reintroducing a phase marker, reintroducing a numbered citation, emptying the file list, registering a claim against an unscanned file, deleting the observing needle, and planting a phase marker in the guard's own source. 283 → 285. -
property resolution's read count and failure identity are now asserted (tcinc671). Seven tests in
resources_test.dart, over three fake config providers. A counting provider pins that resolving one key performs exactly one lookup on all three branches — value present, default used, cast failed — which is what makes "reads the store once" a contract rather than a claim about the current implementation. A provider that answers its first lookup and fails every one after turns the redundant re-read from a structural hazard into an observable defect: before the fix it lost the type-mismatch diagnosis entirely, because the second read threw while building the message that was about to describe the first failure. A provider whose every lookup fails pins that a read failure surfaces as itself and is never dressed as a cast failure — the only witness the narrowon TypeErrorhas, since after the fix nothing else inside thetrycan throw. Two more assert the codes and that theTypeErrorarrives asrootException.Each was confirmed load-bearing by mutation: restoring the
exists/getTextpair fails the two read-count tests, re-introducing the re-read in the message fails the re-read pair, widening the catch and pulling the read back inside thetryfails the unreadable-provider test and only that one, and droppingrootExceptionor changing either code fails exactly the tests naming them.The tcinc670 guard below needed its extractor updated for the new throw shape, and correctly refused to pass in the meantime: a comment sitting between the
keyand the message literal made one throw site invisible to it, and the vacuity pin caught that rather than letting the guard check half the contract. The comment moved; the extractor now also requires each message to be a single literal, because a message split across adjacent literals is one string in the log and several in the source, so the search that finds it in one does not find it in the other. 276 → 283. -
the documented failures are held to the ones the module throws (tcinc670). Two guards in
resources_test.dart. The first extracts the message literal of everythrow Exception(...)in the source and requires the API summary to carry the fixed fragments around each interpolation — so the summary stays free to write the holes however reads best (<key>,<T>) while the words a reader would grep a log for stay tied to the source. The second compares the number of throw sites with the number of rows in the## Exceptionstable, which is what catches a third failure being added to_getValueand left undocumented. Rewording a throw, deleting a table row and adding a third throw site each fail them; a vacuity pin on the fragment count fires if the throw sites ever stop being recognised, verified by making them unrecognisable.Documenting the failures also required asserting two behaviours that had none.
withDefaultdoes not rescue a wrong-typed present value — the cast branch runs before the default is consulted — which is the asymmetry a reader is most likely to get wrong, and it was untested. And a key whose configured value isnullcounts as absent, becauseTomConfigResourceProvider.existsis "resolves to a non-null object"; in a YAML configapp.region:with no value does not satisfy an obligatory property. The test helper's parameter widened toMap<String, Object?>so the null case is expressible at all. 272 → 276. -
a recommendation stated in four places cannot lose its exception in one of them (tcinc669). The subclass advice had drifted out of step with the code in every artefact at once, which is what a rule repeated four times does when nothing checks it. A guard in
resources_test.dartnow holds all four — the source, the module readme,doc/resources.mdand the API summary — to both halves: each must still recommend the subclasses, and each must name the nullable-Texception. A third test requires the three consumer-facing documents to show the construction, then runs it against the key those documents print, so the link is the snippet executing rather than a text match between documents. That third test was rewritten during verification: as first written it also searched this suite's own source for the construction, which the guard's own string literal satisfied — an assertion that could never fail. Deleting the behaviour group exposed it, and the executable form replaced it. One boundary is deliberate and was verified by breaking it: only the three consumer documents must print the construction, not the source, so the dartdoc example stays free to change. Removing the qualification, removing the recommendation, removing the construction and breaking the_hasDefaultgate each fail exactly the intended test. 269 → 272. -
a module document cannot name a dependency the module has no relationship with (tcinc668). A
## Dependenciessection is the one part of a module document that describes the code rather than the behaviour, so it goes stale in a way prose does not: delete the code that used a type and the line naming it survives, still telling the reader to install it. A guard indocumentation_links_test.dartnow walks all seven module documents and requires every framework symbol named in the dependency bullets to appear somewhere in that module. Two boundaries are deliberate, and each was verified by breaking it. The check reads the bullet list only, because the prose that follows describes what an application must do and legitimately names types the module never touches — widening it to the whole section reportsdoc/preferences.md's correct instruction to register a platform withTomPlatformUtilsas a defect. And it matches against the module's full text, dartdoc included, rather than code alone, because a dependency is legitimately indirect:configurationnamesTomExceptionas the base its exceptions inherit,preferencesnamesTomBearerAuthenticationfor a token its channel attaches andTomRuntimefor the platform its bean locator selects on, and none appears in an expression. The weaker claim — referred to somewhere — is the one with no false positives, and it still catches the failure it exists for, since a line left behind by deleted code is mentioned nowhere at all. 268 → 269. -
the shared store suite runs against a backend that keeps its promises and nothing more, and cross-document anchors are checked (tcinc667).
preferences_test.dartgained a third instantiation of the shared suite, behind a store whoseread()returns its own unmodifiable list — everything the contract permits and nothing it does not. Running the existing 13 contract tests against it, rather than adding two bespoke assertions, is what did the work: with theremovedefect reintroduced the two original backends still reported all tests passed, so the suite had been blind to it by construction. Two further guards pin the ownership rule in the four artefacts that state it. Separately,documentation_links_test.dartnow checks that a link naming a heading in another document finds it — the existing link guard drops the fragment and checks only the path, which is the half that rarely breaks, so a renamed heading left the link silently landing at the top of the right file. Its first run exposed a bug in the anchor helper written undertcinc666:github-sluggerreplaces each space individually, not each run of whitespace, so stripping an em dash out ofA — Bleaves both surrounding spaces and therefore two hyphens. No link had ever been wrong; the helper was. It could not have been caught by the table-of-contents guard, which applies the same function to both sides of its comparison. 252 → 268. -
every document's anchors and table of contents are checked, and the preferences scope rule is pinned in all three places it must appear (tcinc666).
documentation_links_test.dartgained adocument structuregroup with two package-wide guards: every same-document anchor link resolves to a heading that exists, and every module document's contents block lists every one of its##sections, in order. Both had to derive GitHub's anchors, which means decoding HTML entities before stripping punctuation — skipping that step reports five live links in this package as dead, so the helper does it and says why. The contents guard found four genuine omissions on its first run (above). Three further guards inpreferences_test.dartcheck that the module readme and both consumer documents each show the per-accountlocationpattern and each state what the account segment must be, and thatdoc/preferences.mdcarries the rule as a section of its own. They assert the two load-bearing facts rather than any sentence, so the prose stays free to be rewritten. 247 → 252. -
the conditional-export variants of a store are checked as a set, and the checks read declarations rather than the prose about them (tcinc665). The guard that
configurationgained undertcopen198— its two file-store variants must agree aboutconstand declare the same public members — moved totest/support/conditional_export_variants.dartand now servespreferences' three platform-store variants as well. Two bugs surfaced in the move, both the same mistake at different layers: the checks matched raw text, so the dartdoc explaining why a constructor is notconst, and the API summary's paragraph explaining the same thing, each read as aconstdeclaration — a guard that fails on correct code precisely because the code was documented. Sources are now read with comments stripped and summaries only within their```dartfences. Theconfigurationcopy had been carrying this latently and passed only because its explaining sentence happened to wrap betweenconstand the class name; rewrapping that sentence to read better turned it red, which is how it was found. Because "all variants agree aboutconst" could otherwise be satisfied by deleting the neutral variant's throw,preferencesalso gained the two tests pinning that it still fails loudly and still names the location — its neutral variant previously had no test at all, on the one code path whose whole purpose is to be reached. Five new tests inpreferences_test.dart; 242 → 247. -
the API-summary constructor mirror is one shared check over three modules (tcinc664).
little_things_test.dartcarried a parser that reads a constructor's parameters out of a source file, resolves the kernel'sthis.xforms to their field types and compares the result against the summary's code fence.preferencesandconfigurationneeded exactly the same check, so rather than a second and third copy the parser moved totest/support/exception_constructor_mirror.dartand all three modules call it;little_things' four tests are unchanged in name and in what they assert. The class list each module checks is now derived from its sources (exceptionClassesIn), so an exception added to a module is covered without anyone extending a hand-written list — and a guard cannot be quietly satisfied by shortening one. Four new tests, three inpreferences_test.dartand one inconfiguration_test.dart. Verified by mutation: droppingautoLog's default, restoringbool?, deleting a constructor block, and making the summary sayObject? stackeach fail exactly the intended test. -
observing: two lifecycle test comments describe the contract instead of the TDD phase they were written in (tcinc663). Both comments in
test/observing_test.dartended by declaring the test red against an implementation that lackeddisposeand lackeddidUpdateWidgetmigration — the strongest possible claim about a module whose entire contract is those two hooks, made directly above two passing tests, whiletom_observing_widget.dartdefined both. They were accurate red-first markers that the change turning them green did not remove. Each now names the hook it pins and explains why both assertions are needed: adding the new registration without dropping the old renders correctly and is visible only as a rebuild the old observable should no longer be able to cause. Both comments also opened by citing a numbered section of a document that is nowhere in the workspace; the citations are dropped rather than resolved, matching the direction tcinc672 takes for the same defect in the resources tests. A three-test guard keeps the file free of phase markers and section citations while requiring the comments to keep naming their hook, so it cannot be satisfied by deleting the prose it checks. -
observing: the example demonstrates the deviation it used to rely on (tcinc662).
example/observing/observing_example.dartcalledadaptor.removeListener(() {})with a closure it had never registered and said only "removeListener stops further notifications", so the one line that proved the undocumented half read as an oversight. It now says why that works and adds the mirror case, twoaddListenercalls where the first is replaced before it ever fires.test/observing_test.dartgains the matching pin — the existing removal test hands back the listener it registered, so it passes whether or not the argument is consulted and cannot stand in for it — plus a three-test guard requiring every file that carries the single-listener callout to state both halves. The API summary'sremoveListenerrow is checked separately, because the file-wide check passes as soon as the callout paragraph mentions the argument while the table still says otherwise. -
observing: the reference directions are pinned, without depending on the collector (tcinc661).
test/observing_test.dartgains two behavioural tests and a documentation guard. The behavioural pair states the two directions the prose now distinguishes: an observer held by a local keeps receiving notifications untilremoveObserveris called — removal, not collection, is what ends them — and a mountedTomObservingWidgetstill holds the very observable instance it was given. Neither waits on a garbage collection, so neither is flaky. The guard checks every file in the module that tells a reader whatdisposedoes: each must exist, must still describe the deregistration, must say which side holds which, and must not carry the retired claim again. -
little_things: the catch-clause order is executed rather than only stated (tcinc660). The module exists so that
on X catchclauses can separate a presentable client failure from one that must not be shown, and the single rule a consumer can get wrong is thatTomClientUIException's clause has to come beforeTomClientException's. That rule was written in the class dartdoc, the module readme and the completion document, and demonstrated nowhere: neither the example nor the suite contained a singleonorcatch, both expressing the discrimination with anischeck instead — a mechanism that has no order and so cannot show the mistake. The example now runs the correct handler and the swallowing one side by side and prints what each returns for the same failure, and the suite asserts which clause ran in each, that a kernel failure walks past both client clauses, and that the boundary wrap catchesTomExceptionwhile keeping the cause inrootException. The mistake is spelled as a missing specific clause rather than a reversed pair, because a reversed pair is rejected by the analyzer asdead_code_on_catch_subtypeand therefore cannot ship; a handler that only ever grew the general clause can, and has the same effect. A guard holds the example to the shape — it fails if theonclauses disappear, if the boundary wrap narrows to the client type, or if the general clause is shown first. -
every example now names a command that succeeds (tcinc659). All seven examples told the reader to run
flutter test example/<m>/<m>_example.dart. That command does run the example —main()executes at load time, every print appears, every assert is evaluated — and then exits 79, "No tests were found", because an example declares notest(). So the documented invocation was a failing command: anything reading the exit code saw a broken example that had in fact done exactly what it should. And it exited 79 whether the assertions held or not, so the code said nothing about the example either way. Each header now namesflutter test test/<m>_test.dartinstead — the suite that calls the example'srun()— which exits 0 on success and non-zero when an assertion fails. The premise the old text gave is kept, because it is true and explains why: the barrel import pulls influtter/material, so baredart rungenuinely cannot work. -
the remaining five examples are executed by their suites.
configurationandlittle_thingsalready were;observing,security,resources,runtimeandpreferencesclaimed only that their behaviour was "covered by" a test file, which is not the same as being run by it. Each module suite now ends with athe shipped example runsgroup callingrun(), so the command the header names is a real one.resourcesrestores the app config provider its example installs process-wide, rather than leaving it to the nextsetUp. -
test/example_instructions_test.dartguards the instruction. Four checks: every shipped example is discovered, every example names a suite that exists, that suite imports and calls the example, and no example names the exit-79 form. The first exists because the last three are otherwise satisfiable by deleting the example. -
the little_things example demonstrates
autoLog(tcinc657). The example introduced the forwarded kernel payload as "parameters,stack, andautoLog" and then passed only the first two —autoLogappeared exactly once in the file, in that sentence. It is now shown rather than described, which is the branch the guidelines require (_copilot_guidelines/dart/examples.mduses narrate-instead-of-demonstrate as its BAD specimen), and the only one of the three that can be shown: the other two are inert data, whileautoLoghas an observable side effect. A recording log output stands in for the configured one, an exception is built without the flag and one with it, and the two records the constructor wrote are printed. -
the little_things example is wired into the suite.
run()is now called fromtest/little_things_test.dart, so the example's own assertions — the ones that make itsautoLogdemonstration mean anything — actually execute. Two guards accompany it: the example may not name a forwarded argument it never passes (the forwarded set is read out ofclient_exceptions.dart, so a seventh parameter added tomorrow is covered without anyone extending the test), and, because that guard is satisfiable by deleting the mention rather than fixing it, a second pins thatautoLog: truestill appears in the code. -
autoLogis tested for its effect, not just its field. The two existing tests assertedex.autoLog == trueand stopped there; both would have kept passing if the kernel stopped logging altogether, and both dumped a pair of multi-line stack traces into every green run. They now capture the log, and a new test pins the effect directly: silent by default, two records with the flag,EXCEPTION:thenEXCEPTION_STACKTRACE:, both carrying the exception's uuid so a trace can be joined back to the failure it came from. -
documentation links (tcinc656). A new
test/documentation_links_test.dartchecks every markdown link the package ships. Two properties, because a link fails in two ways and only one of them is a path problem: every repository-relative target resolves on disk, and every link whose label is a single code-span identifier lands on a document that actually mentions that identifier. The second is what catches a link that resolves cleanly onto the wrong document — the defect above passed a path check and would have gone on passing one. Targets rooted at/are skipped and documented as such: they are workspace-root-relative by convention, and the workspace root sits above this repository. A third test pins that the scan still sees the corpus it was written against (files, relative links, identifier-labelled links), because both guards report success on an empty scan; a fourth pins the specific link the suite was written for, including that it has not simply been deleted. -
little_things (tcinc655). Two tests pinning the module readme's framing: that it still describes
little_thingsby the role the directory plays (a landing place for small cross-cutting helpers), and that the paragraph introducing the exception family marks it as the module's present contents rather than its definition. The second accepts any temporal qualifier rather than one sentence — forbidding a phrasing is satisfied by deleting it, and requiring one freezes it — and is paired with a check that the paragraph it watches still exists. The wording is load-bearing: this directory is the same one in all three tom_core packages, and the server's went from two exception classes to also holding a request-id type and a wire-envelope helper inside a fortnight. A readme that reads as "the exceptions folder" is how the next client-side helper ends up parked somewhere more specific. All three failure modes were mutation-checked. -
little_things (tcinc654). Four tests in a new
the API summary mirrors the little_things sourcesgroup. These classes declare no types of their own — every parameter arrives bysuper.x— so the summary's types are only checkable against the kernel constructor they resolve to, and reading the field declaration instead gives a wrong answer that looks right. The tests therefore parseTomException's constructor out of the kernel source (resolvingthis.xforms to the field's type and default) and compare it with the summary's blocks: one checks the parameter names and their order against whatclient_exceptions.dartforwards, one checks every type and default, one is a meta-test pinning that the kernel parser still reportsStackTrace?,bool = falseandMap<String, Object?>?— without it the first two pass just as happily on an empty parse — and one walks theextendschain out of the sources and requires every base it finds to appear in the hierarchy diagram. Each was mutation-checked by reintroducing the defect it guards. -
configuration (tcinc653). One test pinning the order
save()writes: carried-forward undeclared keys first, then the overridden settings in declaration order. Nothing depends on that order — a JSON object is unordered either way — but the completion document quotes the printed map literally, and a reader compares their own run against it, so an order that drifts silently turns the quotation into a false report. The test overrides its two settings in the reverse of declaration order, so passing cannot be an accident of the order the test wrote them in. -
configuration (tcinc652). Five tests closing three coverage gaps that a documentation claim had been standing in for. Two
testWidgetscases bind aTomSetting<bool>straight to aTomObservingWidget— one taps theSwitch, the other changes the setting from outside and pumps — which turns "aTomSettingis a legalsubStatewith nothing adapting it" from a claim about a type bound into an assertion. Two cover the neutral file store: it is selected by conditional export, not conditional compilation, so its source can be imported directly under a prefix and its constructor exercised on any target; the module's one deliberate fail-fast path was previously a promise no test checked. The fifth runs the example itself, so its snippets are executed documentation rather than transcribed documentation.The example gained an eighth step: a real
TomFileClientConfigurationStoreround trip over a temporary directory, reopening the same path with a second holder to assert it adopts the first's override while still re-resolving the untouched setting from the shipped config resources — the cross-holder round trip an in-memory store cannot demonstrate. Two "a real client would useTomFileClientConfigurationStoreinstead" comments are gone, replaced by the thing they described. -
configuration (tcinc651). Four tests in a new
the module documents validation truthfullygroup. Two are documentation guards: no artefact defers validation toCs*whiletom_code_specsis absent from the pubspec (conditioned on the pubspec, so adding the dependency would make the pointer legitimate and the guard stops objecting), and the paragraph that discusses validation must name the caller — a guard that only forbade the old wording would be satisfied by deleting the paragraph. The check is paragraph-scoped rather than file-scoped because both artefacts use "caller" in unrelated passages, so a whole-filecontainspassed however the paragraph read. A meta-test pins that the splitter returns something non-empty, short and on-topic, since one that returned the whole file would pass forever — and it earned its keep immediately, catching that a markdown bullet list has no blank lines and so came back as one 21-line block; a top-level-now starts a paragraph, because in a list the unit a reader takes as one statement is the bullet. The fourth asserts the behaviour the prose now claims: a setting accepts-1as a refresh interval and''as a URL, while a non-numeric raw value still throwsclient_configuration.value.type_mismatch. -
configuration (tcinc650). Six guard tests, in two groups.
the two file-store variants present one signatureasserts that the io and neutral variants agree aboutconstand declare the same public member names — both conditioned on the sources rather than hard-coding an answer, so they stay true whichever way a future decision goes.the API summary mirrors the configuration sourcesparses the three module sources for public members and fails on any the summary omits, checks the file-store constructor line reproduces the source'sconst-ness exactly, and checks every class with a public constructor actually shows one (a name-level check cannot see a missing constructor when the class name is already a heading). A companion meta-test pins that the parser still finds real members, because a coverage test that silently stopped parsing would pass forever. -
configuration (tcinc649). Six tests: carry-forward of a key written by another writer after
load(), and a newswapping the storegroup covering that the previous store's keys are not deposited into the new one, that the new store's own undeclared keys survive (the case that rules out the narrow "just clear the snapshot in the setter" fix), that the previous store is left untouched, that a setting declared after the swap resolves to its baseline rather than the old override, and that re-assigning the same store keeps the loaded snapshot. The executable example gained the carry-forward and store-swap steps. -
Full-surface scaling (RCF3). Closed the remaining public-surface gaps in the per-module test and example coverage (no public API change; 39 → 47 test cases across the 5 module suites).
- little_things: added forwarding tests for the two constructor arguments
that were previously unexercised —
stack(the raw trace object, plus the derivedstackTracestring) andautoLog— on bothTomClientExceptionandTomClientUIException; extended the example to showparameters+stackforwarding. - resources: added coverage for the
withDefaultnamed constructor of all four typed subclasses (TomStringProperty,TomIntProperty,TomDoubleProperty,TomBoolProperty) across both the present-key and absent-key branches (previously onlyInt/Stringwere exercised); extended the example withTomDoublePropertyand a present-wins-over-default case. - runtime: added tests for the previously-uncovered
TomFlutterPlatformUtilssurface —getBrowserLocation()(null on the native branch),toString()(identifies the IO variant), andout/outError(side-effect-only, no throw); extended the example accordingly. - observing and security already covered their full public surface (no additions needed).
- little_things: added forwarding tests for the two constructor arguments
that were previously unexercised —
1.2.0 #
Removed #
- Legacy forms module (RCF2). Removed the back-compat
formsmodule (TomFieldOld,TomEmailField,TomPasswordField,TomFormOld) — its two source files, both barrel exports, the executable example (example/forms/), the test suite (test/forms_test.dart), and the module docs (doc/forms.md,doc/api_reference/api_summary_forms.md). These primitives were superseded by the declarative forms framework intom_flutter_ui(TomForm+TomFormStringInputet al.); per the no-back-compat policy the old surface is deleted rather than deprecated. The sole in-workspace consumer, thetom_uam_clientsample app, was migrated ontotom_flutter_ui'sTomFormin the same pass. README, CHANGELOG, and the API summary index were updated to drop the module.
1.1.1 #
Documentation #
- API reference (OT6). Added a per-module API summary set under
doc/api_reference/following_copilot_guidelines/dart/api_summary_creation.md— oneapi_summary_{module}.mdper module (forms,little_things,observing,resources,runtime,security) plus anapi_summary_index.mdwith quick navigation, per-module type tables, and an alphabetical type index. Each summary lists only the public surface (constructors, properties, methods, operators, and top-level functions) cross-checked against the actual source and the package barrel. The runtime summary documents the single exported getterclientPlatformUtilsand the three compile-timeTomFlutterPlatformUtilsvariants (io / web / neutral). No source or public API change.
1.1.0 #
Verified #
- OT2 refactoring pass (2026-07-15): confirmed the whole-package gates —
dart analyzeclean andflutter test --concurrency=159/59 green — and that every review finding is resolved (seedoc/review_findings.md). No code change; deeper per-module surface coverage tracked as quest todo RCE3.
Documentation #
- Forms module: added dartdoc to
TomFieldOld,TomEmailField,TomPasswordField, andTomFormOld; a module guide atdoc/forms.md; an executable example atexample/forms/; and the first test suite attest/forms_test.dart. No public API change (removed only dead comments). - Little Things module: added dartdoc to
TomClientExceptionandTomClientUIException; a module guide atdoc/little_things.md; an executable example atexample/little_things/; and tests attest/little_things_test.dart. No public API change. - Observing module: added dartdoc to
TomObservingWidget,TomObservingWidgetState, andValueListenableObserver; a module guide atdoc/observing.md; an executable example atexample/observing/; and tests attest/observing_test.dart. Corrected the README usage snippet (thesubState:/child:API; the old snippet referenced non-existentobservable:/builder:parameters). No public API change. - Resources module: added dartdoc to
TomPropertyand its typed subclasses (TomStringProperty,TomIntProperty,TomDoubleProperty,TomBoolProperty); a module guide atdoc/resources.md; an executable example atexample/resources/; and tests attest/resources_test.dart. No public API change. - Runtime module: added dartdoc to
clientPlatformUtilsand the three conditional-importTomFlutterPlatformUtilsvariants (native IO, web, neutral); a module guide atdoc/runtime.md; an executable example atexample/runtime/; and tests attest/runtime_test.dart. No public API change. - Security module: added dartdoc to the
TomClient2FAAdaptormarker annotation; a module guide atdoc/security.md; an executable example atexample/security/; and tests attest/security_test.dart. Confirmed the module is exported from the package barrel. No public API change.
Changed #
- Runtime module cleanup: removed dead code in
platform_detection_flutter_io.dart— an orphaned@overrideannotation, a commented-out_maxCacheSizeconstant, and a commented-out CupertinoClient block with two vague TODOs, replaced by a single concise note recording the deferred native-client intent. No behavior change.
Removed #
camera/placeholder module: the folder contained only an aspirationalreadme.mdwith no implementation and was referenced by nothing. Removed rather than implemented — camera capture is a substantial feature (plugin, platform channels, capture/switch/video) outside the scope of this pass.localstorage/placeholder module: contained only areadme.mdof research links (shared prefs / secure storage / hive / sqflite / objectbox) with no implementation and no references. Removed rather than implemented — a storage abstraction is a substantial feature with no current consumer.preferences/placeholder module: contained only areadme.mdof research links (local-storage / database-adapter options) with no implementation and no references. Removed rather than implemented — user preferences would be a thin layer over a not-yet-chosen storage backend, with no current consumer.qrcode/placeholder module: contained only a three-line aspirationalreadme.md(generate/scan QR, camera integration) with no implementation and no references. Removed rather than implemented — QR generation and scanning require a plugin and camera/image-decode integration (a substantial feature), with no current consumer.
1.0.0 #
- Initial version.