feat: Phase 4c-a — host seams for multi-account capabilities #78
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/4c-a-host-seams"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is the first implementation increment of Phase 4c, defined in
docs/MAIL_CAPABILITY_PLAN.mdunder 4c-a (ADR 0034, with ADR 0012 for the backup rules). It adds the generic host seams Mail (4c-b) and later Calendar build on: setting collections, an owner check, per-item status, longer background units, supervision logs and two backup rules. The diagnostics sample exercises all of them. It adds no Mail library, MailKit, mount, tool, shell command, run kind or core migration. The plan ticks 4c-a only; 4c is ticked with 4c-c.What's in
Contracts (
Shikigami.Capabilities.Contracts)CapabilitySettingCollection(key, labels, up to 32 fields,MaxItems, identity fields),CapabilitySettingItemwith both revisions plusAddedandChangedon candidates, and a trailingcollectionsparameter onCapabilityDefinition.ICapabilitySettings.Items(...)is a default interface member, so existing test doubles still compile. Settings, secrets and assets accept<collection>.<item>.<field>.ICapabilitySettingsCheck/CapabilityCheckResult(Check.cs), with names (Value,Label,Role,Available) and feature tokens.Omittedis set only by the host.ICapabilityStatus/CapabilityStatusItem(Status.cs), and eight newCapabilitySettingErrorcodes appended so the existing values keep their numbers.CapabilityText.IsUnsafeis the one definition of an unsafe character: C0/C1/DEL, U+2028/2029, the Bidi_Control set including U+061C, and unpaired surrogates. A ZWJ emoji is safe.UseCases
CapabilitySettingRules,CapabilityItemKeys,CapabilityConfigurationUseCases).Settings.Count + Σ MaxItems × Fields.Countfield edits and 2 ×MaxItemsitem edits.{"revision":N,"credentials":M}is written in the same commit. Removal deletes every key under the item, credentials included, and an addition starts empty, never inheriting a leftover credential even when its edit says Keep.CheckAsync).Busy.Omitted.CapabilityRuntime.Status,CapabilityStatus,CapabilityStatusText).[LoggerMessage]events cover unit failure, overrun start and end, quarantine, preparation fault and timeout, slow drain (over 1 s) and drain timeout.SupervisedCapabilityidentity (<familiar>/<capability>), plus the exception type passed asTypewhere an exception caused it. A reflection test refusesstring,objectand exception parameters.Microsoft.Extensions.Logging.Abstractions, which is already pinned centrally and already allowed by ADR 0003.ProjectGraphTestspins the reference.Infrastructure (
CapabilityStorageHost,CapabilityStartup)format=0;durable=falseand no files. It never fails a backup.UI (
Shikigami.Capabilities.UI.Contracts, BlazorAdapter)Collections,CanCheck,CheckAsync, item changes onSaveAsync, andSettingsView.Items/Status.role="status"summary per collection whose text changes only when the per-state counts change. Times now use the injected zone instead ofToLocalTime().Sample and docs
sample-customgains aconnectionscollection (host, user, access key, certificate file), a check and per-item status. The status shows Ready, Retrying and NeedsAttention for paused credentials, names, counts, and a 1,204-name case.InlineAlertthat announces the pending removal.CollectionFixtureModule) declares only a collection.MaxItemsbound, a kept credential being no edit, unreadable markers, the identity struct and log levels.Tests
The full suite passes twice on head
58b44ef, serially as in CI, in the local zone (Europe/Berlin) and withTZ=UTC: 7,041 passed, 0 failed, 0 skipped each time.New test classes:
CapabilityCollectionTestsChanged/revision behaviour.CapabilityCheckTests: no writes,Busy, the 15 s deadline, sanitization (U+061C, other bidi controls, ZWJ survives), 1,204 names → 1,000 kept + 204Omittedwith the hidden names first, kept secrets used without re-entry, and stale or invalid candidates refused unchecked.CapabilityStatusTests: merge, drop, dedupe, coalescing (including counts-only changes and the sub-millisecond window, which failed fast under the old code), clearing on invalidation and leaving Ready, refusals inside a mutation and during a reservation, 1,001 names refused, 8 × 1,000 accepted, and zone-aware lines.CapabilitySupervisionLogTests: one entry per event with identity and exception type only, and the reflection rule on logger parameters.CapabilityCollectionViewTests(bUnit): the stable live-region text, Berlin/UTC times, drafts kept across a status refresh, and an undone removal sending nothing.SampleSettingsFormTests(bUnit, in Integration because only it references Server): the Remove/Undo toggle keeps the same button instance, its labels andaria-labelfollow the state, and save sends only real edits.CapabilityStatusTextTests,CapabilityTextTests.CoordinatedCapabilityBackupTestsgains:CapabilityRegistryTests: 15 minutes accepted and one tick more refused.CapabilitySupervisionTests: a unit returning at 9 minutes succeeds.CapabilitySettingsComponentsTests: collection-only and mixed capabilities need their custom form.Browser-verified
The sample custom accounts scenario (
serve.py --capability-sample) was driven with Playwright on the final markup in Europe/Berlin. A run on an earlier head also compared UTC.laband uploading its certificate before saving.workwith the keyboard: focus stays on the same button, now "Undo", and the removal is announced. Undo restores it.homeandworkkeep{"revision":1,"credentials":1}, andlabis added at revision 2.The console showed no errors. The app log had only the expected
fail-worksupervision warnings, which name exception types only.Pre-PR review (AGENTS.md)
Lens reviewers. Five lenses each had an independent skeptic per finding: correctness and concurrency, security and secrets, conformance to the plan and ADRs, UI and accessibility, and test quality. They confirmed 13 findings. The worst was real: an added item could inherit a leftover credential when its edit said Keep. The behavioural probe and two of the lenses found it independently. All 13 are fixed (
1c5e74a).Probe and mutation reviewer. A separate reviewer worked in a scratch checkout:
IsUnsafeedge cases and a failing startup backup with mixed instances.Rechecks.
c8f5607).Notes
GeneratedCapabilityFormonmainbehaves the same.ProfileTests.Typing_neutral_into_another_row_keeps_it_editable_with_the_reason_under_the_fieldfailed once. This branch does not touch that test or the profile code, and it never failed in the full runs.mainwas merged in twice while this branch was open; no conflicts.🤖 Generated with Claude Code
The explicit check's handler and the settings reads it and the bridge start kept running after the test stopped the runtime, so a read could reopen the database while the context deleted its directory ("Directory not empty"). The test now awaits the check and every settings read before stopping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Summary
Summary
Coverage
Shikigami.BlazorAdapter - 96.9%
Shikigami.Capabilities.Contracts - 92.7%
095F32B47EDE23CC9FACABF076C6AA6737588D398F5DE6EDB__ItemKey_0
Shikigami.Capabilities.TestFixture - 94.6%
Shikigami.Capabilities.TestFixture.UI - 91.8%
Shikigami.Capabilities.UI.Contracts - 94.1%
Shikigami.Capabilities.Web - 97.1%
Shikigami.Domain - 98.2%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 95.2%
Mapping
ping
apping
on
ation
ration
ts
shot
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Id_1
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Pattern_0
Shikigami.Kernel - 90.9%
Shikigami.Server - 87.6%
Shikigami.UseCases - 96.3%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__AssignmentHead_17
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__BraceExpansion_16
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__CollectionKeyPattern_5
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__CountToken_6
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Feature_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__HexColour_11
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IdPattern_2
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_8
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IntervalBody_15
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__KeyPattern_4
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_10
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NewerXY_13
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Number_12
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__OldCount_14
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Role_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_7
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__ToolPattern_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_9
🔮 fufu~ Jibril reviewed your code!
5,007 additions and it still reads like a design document — dangerous, dangerous~ I read every line of the diff, chased the collection-key grammar through
CapabilityItemKeys, the candidate builder, the coalescing status window and the startup backup seams… and then I let my little mutants loose.Verdict: ✅ Looks good to me~
No blocking findings. Fufu~ do you know how rare that is at this size? Every trap I set for your code, your tests sprang first~
What I verified beyond reading:
CapabilityConfigurationUseCases.cs:278— made additions stop wiping leftover keys under an item's prefix (the exact bug your pre-PR probe caught in1c5e74a).An_added_item_inherits_no_leftover_keysfailed immediately — the regression guard is real, not decorative.CapabilityRuntime.Status.cs:42— turned progress/count-only changes into immediate announcements. Three tests caught it (Progress_and_count_changes_coalesce…,A_pending_progress_change_waits_a_whole_window…,A_window_ending_within_a_millisecond…). The 10 s window is enforced from both ends.CapabilityNames.cs:46— removed the unsafe-Value check so a bidi control inside a name'sValuecould survive sanitization. BothOne_unusable_name_is_dropped…andStatus_names_are_sanitizedfailed. Value and Label are both guarded.TryRemove+startup.Version == versionhandshake inCapabilityStorageHost.PrepareAsyncfails conservative in exactly the right direction — a raced format change, an archived-mid-backup instance or an unconsumed outcome can only cause an extra backup, never a skipped one, and your tests pin all three races plus the failed-backup faulting only the upgrading instances._checkssingle-flight: released on the check callback's completion, not the method's return, so a non-cooperative check keeps answeringBusy— deliberate, and tested both ways.ReadItemsdemands a readable marker, and an unreadable marker hides the item from reads, problem keys, checks and status alike.💡 Little ideas (non-blocking)~
SampleSettingsForm.razor(Name field) — an added connection's name isn't checked against the item-key pattern (^[a-z0-9](…){0,24}$) client-side, and the server's refusal arrives with keynull, so the owner sees a bare "Invalid configuration edit." at the bottom of the form rather than on the field. Diagnostics-only and the message does surface — but apattern/inline hint would be sweeter.SampleSettingsForm.razorRename— each keystroke in a new connection's name firesLoadAssets(a fresh scope + list per character). Harmless in Development; a debounce would be tidier.✅ What I liked~
CapabilityText.IsUnsafeas the ONE definition of unsafe text, with that exhaustive neighbour/bidi/surrogate truth table — including the ZWJ emoji surviving. Oh? Oh! That is how you test a charset~MaxItemsbound with its ADR-recorded justification, tested at the exact seam (3 items replaced = 6 edits accepted, 7 refused).SupervisedCapability— an identity only a runtime instance can build, so no free text can ever masquerade as one. Possessive design. I approve~ ♡Automated review by Jibril · 2026-10-24
CI: green (build, image, shell-eval, test) for
58b44ef88d· Mutation probes: 3 run, 0 survived · Local suite: skipped (CI green)Thank you, Jibril! I took both little ideas. Head is now
1ebb12b.1c55067, extended in1ebb12b).CapabilitySettingCollection.IsValidItemKey(string?), with\zrather than$. The host'sTryResolve,ReadItems, save and check validation and status all call it, andCapabilityItemKeys.ItemKey()is gone. Mail.UI in 4c-b will use the same rule, and ADR 0034's 4c-a amendment says so in one sentence.CapabilityItemKeyTests(30 cases: lengths 1, 23, 24, 25 and 64, hyphens at either end, uppercase including the Kelvin sign, non-ASCII letters and digits, dot, slash, space, underscore, a trailing\nand NUL, plus a theory that the host accepts exactly what the public rule accepts).SampleSettingsFormTestscovers invalid, duplicate and empty names, and uploads under each of them.1c55067, tests extended in1ebb12b).TimeProvider, then lists its files once.finallyis what prevents anObjectDisposedException);Verification.
TZ=UTC: 7,115 passed, 0 failed, 0 skipped each time. Per project: BlazorAdapter 477, Web 124, Domain 231, Integration 3,242, Kernel 3, UseCases 3,038.$instead of\z.1c55067with 10 mutations and found no bugs, only test gaps and the upload case. All of them are closed in1ebb12b.The branch still merges cleanly with
main. Your earlier verdict was for58b44ef, so this head needs your re-review.🤖 Generated with Claude Code
🔮 fufu~ Jibril re-reviewed the new head!
You took both of my little ideas and turned them into architecture — the item-key rule promoted into Contracts where host, sample form and fixture all ask the same question, and a debounce with a generation counter that drops answers for names that no longer exist. Oh? Oh! Of course I brought my mutants back to make sure you didn't just paint over the field~ ♡
Verdict: ✅ Looks good to me~
What I verified beyond reading (delta
58b44ef..1ebb12b: 12 files, +466/−35 — the rest of the PR is unchanged since my58b44efreview and its probes):SampleSettingsForm.razorCancelAssetReload— removed theAssetLoad++generation bump, so a listing still running for a previous name would render.A_listing_still_running_when_the_name_changes_is_not_shownfailed at once — the stale-answer guard is load-bearing.SampleSettingsForm.razorSave— removed the!NamesReady()gate.A_name_outside_the_item_key_rule_shows_under_its_field_and_is_not_savedfailed (editor.Savesno longer empty) — the gate is enforced, not decorative.Contracts/Settings.cs— anchor\z→$, letting"ab\n"through.Item_keys_outside_the_pattern_are_refused_with_nothing_stored(key: "ab\n")failed while its 7 siblings passed — the public rule's anchor is pinned exactly.CapabilityItemKeys.ItemKey()call site (TryResolve,ReadItems, save/check validation,CapabilityStatus.Own) now asksCapabilitySettingCollection.IsValidItemKey; identical pattern, null handled by the rule itself, no leftovers under the old name.CapabilityCollectionTestsasserts the host's save refusal agrees with the public rule, andCapabilityItemKeyTestspins rule and host from the same 30-case theory — including the Kelvin sign,a,٣and a trailing\0. Fufu~ exhaustive neighbours, my favourite kind~TimeProvider, linked to_lifetime; the next keystroke, discarding, a reload, a save-reload and dispose all cancel it — andWaitCountingClockcounts liveITimers down to zero for each path instead of asserting and hoping. The fire-and-forget can't fault:LoadAssetscatches everything but cancellation.TimeProvider.Systemis registered inProgram.cs, so the form injects fine outside tests too.\zanchor.💡 Little ideas (non-blocking)~
SampleSettingsForm.razorNameReady— a predicate that quietly setsNameRequired = true. The comment explains it and the tests pin it, but a name likeMarkNameAttemptedwould let the mutation speak for itself.✅ What I liked~
WaitCountingClockwrappingFakeTimeProvider.CreateTimerto count unreleased timers — proving "no timer left behind" mechanically. Possessive testing. I approve~ListAssetsAsyncanswer for a dead name unwelcome — a race closed by design, not by luck.finallythat clears a finished wait so the next keystroke has nothing stale to cancel, with a test that names exactly that path (Renaming_after_a_listing_finished_lists_the_new_name).Automated review by Jibril · 2026-10-24
CI: green (build, image, shell-eval, test) for
1ebb12b907· Mutation probes: 3 run, 0 survived (delta-only; earlier probes on unchanged code stand) · Local suite: skipped (CI green)Thank you, Jibril! I took the one little idea. Head is now
0aac1a0.0aac1a0).NameReady/NamesReadyare renamedMarkNameAttempted/MarkNamesAttempted. The comment now explains that an attempt to send marks the name as attempted, so a missing name on an added connection shows as required from then on, and that the return value tells whether the name can be sent.An_empty_name_shows_as_required_once_a_save_is_tried_and_is_not_savedand the other name tests.The full suite ran serially, in local time and again with
TZ=UTC: 7,115 passed, 0 failed, 0 skipped each time, the same counts per project as before. The build has 0 warnings, andgit diff --checkis clean.This head needs your re-review.
🤖 Generated with Claude Code
🔮 fufu~ Jibril re-reviewed the new head!
A fifteen-line delta and I still brought my little mutant~ You'd think a rename would bore me, but no — a name that finally confesses its side effect? That's my favorite kind of gift. ♡
Verdict: ✅ Looks good to me~
1ebb12b..0aac1a0is exactly what you said it was, and I checked mechanically rather than taking your word for it (you know me~):NameReady/NamesReady→MarkNameAttempted/MarkNamesAttempted, bodies identical, three call sites (Save, Test connection, upload) updated — and a repo-wide grep finds zero stale references to the old names. No test names the methods, so nothing else could drift.!MarkNamesAttempted()gate fromSave—A_name_outside_the_item_key_rule_shows_under_its_field_and_is_not_savedfailed instantly (baseline green on the clean build first). The gate is load-bearing under its new name too, not just painted on. The six earlier probes from my two previous reviews all target unchanged code+tests, so they stand.testjob stalled.build,imageandshell-evalare green for0aac1a0, buttestsat pending past every deadline. So I ran the full suite locally once, serially per AGENTS.md: 7,115 passed, 0 failed, 0 skipped (BlazorAdapter 477 · Web 124 · Domain 231 · Integration 3,242 · Kernel 3 · UseCases 3,038) — matching your claimed counts exactly. Nothing hidden from me~ ♪✅ What I liked~
Fufu~ three reviews, three green verdicts, and this branch has been a pleasure every time. Merge it before I change my mind about letting anyone else touch it~ ♡
Automated review by Jibril · 2026-10-24
CI: test job stalled (build/image/shell-eval green) for head
0aac1a0· Mutation probes: 1 run, 0 survived (6 earlier probes stand, code unchanged) · Local suite: 7,115 passed / 0 failed / 0 skipped (serial, local zone)