#161 批量查阅: neutral read-only Query, 首页乙/墨与米 shell, interpretation publication status - #177
#161 批量查阅: neutral read-only Query, 首页乙/墨与米 shell, interpretation publication status#177davidqyc wants to merge 11 commits into
Conversation
Characterize the existing behavior most at risk from the #161 renovation, proven green against unmodified production code: - a candidate Token that fails validation, or fails the Keychain save, leaves the old active connection and the stored Token untouched; - the phrase binding context's expected status is the legacy shared PUBLISHED constant, which must not become a dynamic preference; - 清空历史 clears both interpretation and phrase receipts from the one store, so the contextual presentation cannot narrow the destructive scope; - a committed golden pre-change History archive — including a receipt written before contentKind/unconfirmed/diagnosticEnvironment existed — decodes in full, so the planned optional interpretation-status field cannot require a migration or an archive-version bump; - the three Capture accessibility identifiers used by physical and simulator regression tests are pinned as a shared constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(Checkpoint 1)
Small domain/API/write-safety extension, ahead of any shell wiring.
Interpretation publication status (公开 / 未发布):
- new InterpretationPublicationStatus + device-local preference, mirroring
WriteTagPreference; default .published reproduces today's behavior and
today's binding digests byte-for-byte;
- the status is threaded through the existing chain — Preview snapshot →
binding context → batch/binding digest → fresh authenticated preflight →
request body → authenticated readback — not a new approval stack;
- makePlan's pin to CompanionConstants.status becomes an explicit
{PUBLISHED, UNPUBLISHED} allowlist; anything else fails closed before a
digest, body or POST exists;
- matchesIntendedState gains an intended-status dimension, so same text +
same tags + different status classifies as 更新, never 一致;
- a readback that does not reproduce the approved status is the existing
intendedStateMismatch → notVerified, and is never retried;
- CompanionConstants.status stays immutable for the phrase/legacy path;
ExecutionReceipt gains an optional, never-backfilled interpretationStatus.
Notes (助记) read, strict and read-only:
- GET /open/api/v1/notes?voc_id=… on the same authenticated transport and
shared scheduler, with a closed decoder; unrecognised envelope is a whole
response rejection, unrecognised record an item rejection, nothing guessed;
- no note mutation route; records are returned whole, DELETED included.
Pure Query core: input parser (newline / , / , only — never space, -, / and
no semicolon in v1), the advisory-only request-budget helper derived from the
real scheduler windows, the count projection (0 is never unavailable), and the
local AND filter where unread/loading/unavailable satisfy no numeric predicate.
Also re-syncs one stale build-number assertion that #176 left pinned at 3
after bumping the project to 4; no build/version/upload state is changed.
370 unit tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Root ownership and coordination, then the headless Query engine. No UI yet. One root owner, one operation lane: - CompanionViewModel remains the single owner of the credential session, the shared RequestWindowScheduler, the write/Preview state and the Capture lifecycle entry; it now also owns an explicit provider operation lane; - the scheduler is a rate ledger, not a single-flight proof, so credential validation, Preview, authorized writes and Query each take the lane before dispatching and cannot overlap. Tests observe the actual fake transport's in-flight high-water mark, not scheduler reservations; - Query's whole access is a QueryReadLease: a transport built from the root owner's credential lease and the shared scheduler, plus the account identity. No Keychain, no second CredentialSession, no second scheduler, no write authority. Account identity is two-dimensional, because a fingerprint alone cannot express both required rules: the Keychain fingerprint, plus an authority generation advanced only by an explicit successful connect / replace / remove. So a background suspension, a same-Token foreground restore, a 401 and a failed candidate all leave account-derived truth alone, while a deliberate reconnect after a rejection clears it even with the same Token. QuerySessionStore is app/process-session scoped above the Query destination: - sequential row-major reads, one request in flight, no concurrency; - the resolver stage is atomic; after it, each content cell is atomic — completed cells are never re-read and unavailable cells never auto-retry; - a run-generation token means a late response from a stopped or superseded run cannot mutate a newer result; - stop preserves completed truth and marks the rest 未读, never 0; - a global failure (401/network/429/server/schema) stops the batch and keeps completed rows; nothing is ever retried automatically; - duplicate input rows cost one read and show the same truth; - detail renders already-returned objects and issues no request; - modify keeps the old result returnable until the first real edit; - memory only — no persistence, no background continuation. AppRouter is navigation and presentation only: one write destination, so switching 释义/例句 is state rather than navigation and .write can never appear twice on the path. 406 unit tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The frozen visual contract, wired to the already-tested domain and session behavior. No write-safety mechanism changed; the surfaces moved. - semantic design roles as asset-catalog colors with Any / Dark / Increased Contrast variants, referenced through a small Theme namespace so no view carries a hex literal; - Home 首页乙: two ink tiles, one ivory 批量查阅 card, one 设置 pill, and nothing else — no account row, metrics or tabs; - Settings owns account management: connect / replace / remove with a destructive confirmation whose copy states the real scope, one 录入偏好 row with a 公开 · 标签 n/3 summary, and About; - 录入偏好: the 释义发布状态 segment and 词签 chips in a native adaptive grid that falls back to a native checkmark list at accessibility sizes; - the write surface keeps every existing editor/Preview/execution builder and gains only the 连接状态 line, the contextual history pill and the frozen detail labels 现有 / 拟写入 / 标签 / 状态; - contextual History filters one store by the mode it was entered with; 清空历史 stays global and says so; - Query: input with the request-budget advisory, the progressive results table with the neutral numeric grammar, the local filter sheet with present-only reasons, row detail from already-returned objects, copy, stop/resume and the running interrupt dialogs; - Capture keeps its proven lifecycle and its three accessibility identifiers, and accepting replaces the path so .write appears exactly once. The root view still owns the one CompanionViewModel, the scenePhase ordering, the Capture pickup gate and all three write confirmations. The app-scoped QuerySessionStore lives above the Query destination, so Home ↔ 批量查阅 restores the result with no request. The source-scanning guard is re-homed rather than weakened: UIPasteboard is now allowed in exactly one file (the Query results view, for 复制当前 N 项) and forbidden everywhere else, and the private-sounding label for UNPUBLISHED is asserted absent from all production sources. 406 unit tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On-simulator regression for the retrofitted shell, plus the three real accessibility defects it caught and fixed. Coverage: - Home exposes exactly the four frozen entries and none of the things the design froze out (no account row, no History summary, no tab bar); - Settings owns account management, reaches 录入偏好 and 关于, and the root summary reflects both preferences; - 释义 ↔ 例句 is a state change, not navigation: the title and history pill follow it and a single back returns to Home, so .write is entered once; - contextual History is reached from the write surface and returns; - Query input states, the disconnected gate, live parse feedback, and the app-scoped store surviving a Home round-trip with no work; - at accessibility Dynamic Type the tag grid falls back to a native list, the 3/3 rule and its hint copy still hold, and disabled why-lines survive. Accessibility fixes the tests exposed: - a grouped row wrapped its Button in an accessibility element, producing a phantom element that absorbed taps without activating the row — and forcing `.accessibilityElement` onto a Button drops its button trait entirely. Rows now set label/value on the Button itself; - the segment control's group label overwrote every segment's own name, so VoiceOver announced "录入模式" three times instead of 释义 / 例句. Segments keep their names and the group is a `.contain` container; - Home tiles and the Query card led with a run-on combined phrase; they now announce the name and carry the supporting lines as the value. The Query input's separator rules are the field's VoiceOver hint, which XCUITest cannot read back, so that copy is pinned in the source guard with the other frozen Query strings. Also restores the Capture review's 抓词 navigation bar and its nav-bar 取消, matching board 15-04 and keeping the existing physical/simulator Capture regression tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…header #161's Home displays `小黑鸟伴侣` as its header, and that header sorts ahead of the Share Sheet's extension row in the accessibility hierarchy, so the release gate's label-only query began resolving to a view the modal sheet was covering. Tightened rather than loosened: the row is now chosen by hittability, which is precisely "the row the Owner can actually tap" and reintroduces no coordinate or row-position assumption. `hittable` is not a valid XCUIElement predicate key path, so the choice is made in code over the matching elements. Full suite green: 406 unit tests, 10 UI tests, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
davidqyc
left a comment
There was a problem hiding this comment.
Physical current-version evidence received before PR adjudication exposes a pre-existing Phrase post-write recovery defect that PR #177 does not currently repair.
Observed flow from Owner video:
- Preview: 5 phrases, 新建 5 / 一致 0 / 阻断 0.
- Execution: 新建成功 1 / 未确认 1 / 未执行 3; stop-after-unconfirmed behavior is correct and must remain.
- Later re-Preview:
shipping invoiceandfactory siteboth become 阻断 /无法安全读取例句状态; remaining three stay 新建. - Maimemo UI independently shows the
shipping invoicephrase actually exists with the intended content, so re-Preview recovery is failing on an already-created record rather than truthfully converging to 一致.
Fresh first-party increment: maimemo/memo-api-cli currently defines Phrase.tags as free-form string[], and its official tests use arbitrary tags such as greeting / updated. Current iOS phraseTags(_:) instead rejects any returned tag outside the app's canonical 22 write-preference tags. This is a strong root-cause candidate for a post-create record becoming itemResponseRejected -> READ_FAILED; the Owner UI also shows a 我创建 chip, but we must not assume that exact UI badge is in the API tags array without raw-response proof.
Important separation: outbound/write tag selection may remain restricted to the canonical app tags. The read decoder should not reject an otherwise safe Phrase merely because the provider returns an additional safe tag string that is outside the app's write-preference vocabulary. PhraseRecord.hardMatches does not use tags for write-target identity; tags are an observation layer.
MERGE_GATE=BLOCK_PENDING_PHRASE_POSTWRITE_READ_RECOVERY
Before merge, either prove a different exact root cause or repair the read contract with focused regression coverage. Minimum high-value test: a valid returned phrase with a bounded/safe non-canonical tag must decode and remain classifiable instead of becoming READ_FAILED. Then perform a read-only physical re-Preview on the already-created Owner items; no new Maimemo mutation is needed.
davidqyc
left a comment
There was a problem hiding this comment.
Coordinator note: the fresh provider evidence is specifically:
maimemo/memo-api-cli/src/types/phrase.ts: Phrase.tags isstring[](no closed tag enum).maimemo/memo-api-cli/test/commands/phrase.test.ts: provider-owned fixtures create/update valid phrases using tags such asgreetingandupdated.
Decision impact: canonical 22-tag validation remains appropriate for outbound user preference/write input, but it is too strict as a universal inbound provider phrase decoder contract. Keep those concerns separate in any repair.
davidqyc
left a comment
There was a problem hiding this comment.
Physical evidence source is Owner-supplied current-version screen recording + Maimemo screenshot from 2026-09-06. Preserve as external/manual evidence only; do not commit the private recording or screenshots into the repo.
davidqyc
left a comment
There was a problem hiding this comment.
Fresh external decision increment for this blocker is complete and decision-relevant: current first-party Maimemo Phrase schema/tests prove inbound tags are not restricted to the app's canonical write-tag set. The repair/review lane may proceed from this frozen evidence; no broad provider research is needed.
davidqyc
left a comment
There was a problem hiding this comment.
Physical regression gate — phrase post-write recovery
Owner supplied a current-production physical recording plus Maimemo UI evidence showing a real safety-recovery defect: a phrase batch wrote at least the first two items, but a later Preview classified those already-created items as 阻断 / 无法安全读取例句状态 instead of recovering to 一致.
Fresh first-party Maimemo evidence materially narrows the likely cause: Phrase.tags is an open string[] in the current official memo-api-cli type, and the official phrase integration tests use values such as greeting and updated. The current iOS decoder rejects any inbound phrase tag outside the app's local 22-tag write-preference catalog. That couples outbound preference validation to inbound provider schema and can reject an otherwise valid existing phrase.
This review does not claim the screenshot label 我创建 is definitely a raw API tag; raw payload was not captured. The decoder contract is independently over-restrictive from first-party evidence and must be repaired regardless.
Required repair direction:
- keep the local 22-tag/max-3 rules for what this app lets the user write;
- inbound phrase decoding must accept safe provider-returned string tags without requiring membership in that local catalog;
- keep fail-closed type/control-character/body-size validation;
- preserve
hardMatchesidentity semantics (tags are observations, not phrase identity); - add a regression proving a matching PUBLISHED phrase with a safe non-local tag decodes and Preview classifies it
一致rather thanREAD_FAILED; - no mutating POST is needed to verify this repair.
MERGE_GATE=BLOCK_PENDING_PHRASE_POSTWRITE_READ_RECOVERY
PHYSICAL_CANARY_AFTER_REPAIR=read-only re-Preview of already-created items; expected first written items -> 一致, never duplicate POST.
davidqyc
left a comment
There was a problem hiding this comment.
Repair dispatch checkpoint — 2026-09-06
EXTERNAL_INCREMENT_ROUND=XHN-161-PHRASE-POSTWRITE-RECOVERY-20260906
SEARCH_DATE=2026-09-06
DECISION_ATTACKED=Whether the current post-write re-Preview block is a provider-visibility problem or an app-side read/decoder recovery defect.
FRESH_EXTERNAL_SOURCE=https://github.com/maimemo/memo-api-cli/blob/main/src/types/phrase.ts ; https://github.com/maimemo/memo-api-cli/blob/main/test/commands/phrase.test.ts
NEW_EXTERNAL_FACT=Current first-party Phrase.tags is an open string[] and official tests use safe tag strings such as greeting/updated outside this app's local 22-tag write-preference catalog.
LOCAL_CURRENT_FACT=Current iOS phrase decoder rejects inbound tags unless every tag belongs to that 22-tag local catalog; PhraseRecord.hardMatches itself does not use tags for phrase identity.
DECISION_IMPACT=Run a narrow same-branch repair: separate inbound provider tag decoding from outbound local tag preference validation, add a post-write/re-Preview regression, keep PR #177 Draft/unmerged, then perform a read-only physical re-Preview of the already-created canary items.
EXTERNAL_INCREMENT_STATUS=PASS
RETURN_BRIDGE_STATUS=READY_FOR_OWNER_RELAY
TASK_ID=XHN-161-PHRASE-POSTWRITE-RECOVERY-20260906
TARGET_AGENT=Claude Code / Opus 5 / Extra / Standard / Single Agent
TARGET_CONVERSATION=CONTINUE_EXISTING
TARGET_CONVERSATION_TITLE=【XHN】#161 Retrofit Implementation
TARGET_WORKSPACE=/Users/david/Documents/GitHub/momo-moreEfficient
TARGET_BRANCH=claude/issue-161-batch-inspection
TARGET_PR=177
EXPECTED_PRE_REPAIR_HEAD=622c769c90c1b694b7da2cd204cab7c1f0a71dca
PROMPT_PREPARED=yes
PROMPT_VISIBLE_TO_OWNER=unknown_until_actual_delivery
EXTERNAL_AGENT_DISPATCHED=unknown
AGENT_RUN_COMPLETED=unknown
RESULT_RETURNED=no
RESULT_INGESTED=no
DUPLICATE_DISPATCH_ALLOWED=no_by_default
RETURN_CAN_RESUME_IN_FRESH_CHAT=yes
CURRENT_AUTHORITY=PR #177 reviews + Issue #161
Do not merge PR #177 before this repair and the bounded read-only physical canary are adjudicated.
…161) An already-created phrase failed to recover on re-Preview: it came back as 阻断 / 无法安全读取例句状态 instead of 一致. Root cause: MaimemoTransport.phraseTags required every returned tag to be a member of the app's local `documentedTags` write catalog, and capped the array at that catalog's length. That conflates two different contracts. The local 22-item catalog governs what the user may SELECT and WRITE; the provider's inbound `Phrase.tags` is an open `string[]` (first-party memo-api-cli src/types/phrase.ts), and its own phrase integration fixtures return values such as `greeting` / `updated` that are outside the local catalog. A valid provider record therefore failed to decode with itemResponseRejected, which PhrasePreflightPlanner catches as READ_FAILED and renders as 阻断. The same decoder backs the phrase readback and the #161 Query 例句 cell, so the defect also degraded post-write confirmation and read-only counts. Fix: inbound phrase tags must be a String array whose elements pass the same bounded safe-string check already used for `origin`; no catalog-derived value or count limit is imposed on the provider. Everything else in the phrase decoder is unchanged — identifiers, duplicate-ID rejection, phrase and interpretation text safety, origin, highlight, the status allowlist and the whole-response size cap all still fail closed. Tags remain observational and are still not part of PhraseRecord.hardMatches, so a matching phrase with a different provider tag classifies alreadyMatching with a .tagsDiffer observation. Interpretation inbound tag semantics are deliberately untouched. Tests: inbound non-local tags decode (including an array longer than the local catalog); malformed shapes and unsafe strings still fail closed; an already-created phrase with a non-local tag recovers to .alreadyMatching / 一致 through the real decoder + preflight rather than READ_FAILED; the local outbound tag preference stays closed at 22 items / max 3; and the Query phrase cell still counts truthfully. Verified as a negative control: the two recovery tests fail against the pre-repair guard. 411 unit tests pass. UI tests not run — no production UI code changed. Physical read-only re-Preview canary still pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Repair — phrase post-write recovery (
|
| Coverage | Result |
|---|---|
Inbound non-local tags decode (["greeting"], ["updated"], mixed, and a 40-element array longer than the local catalog) |
PASS |
| Malformed tag shapes/values still fail closed (non-array, non-String element, nested array, empty, whitespace-only, control character, newline, over-length) | PASS |
Already-created phrase with a non-local tag recovers to .alreadyMatching / 一致 through the real decoder + PhrasePreflightPlanner — not READ_FAILED |
PASS |
| Local outbound tag preference still closed: 22 items, max 3, unknown tag rejected, duplicates rejected | PASS |
Query 例句 cell still counts truthfully and shows the record in detail |
PASS |
| Existing privacy test retained, now driven by a genuinely malformed tag value carrying the sentinel | PASS |
Negative control: with the pre-repair guard restored, testInboundPhraseTagsOutsideLocalWriteCatalogDecode and testAlreadyCreatedPhraseWithNonLocalProviderTagRecoversToAlreadyMatching both fail — the tests close the exact observed failure mode.
TARGETED_TESTS PhraseCreateCoreTests 47 + QuerySessionStoreTests 26 -> 73 executed / 0 failures
FULL_UNIT_TESTS 411 executed / 0 skipped / 0 failures
UI_TESTS not run — decoder-only repair, no production UI code changed
REAL_MAIMEMO_MUTATION_PERFORMED no
IPHONE_MIRRORING_USED no
Still pending
Physical read-only canary remains required and is a Coordinator gate — no write is needed to validate this. Re-Preview the same batch against the already-created canary phrases:
already-created first item(s) -> 一致
later never-written items -> still 新建, subject to actual server truth
no duplicate POST
PR stays Draft; not merged.
Return Bridge — read-only physical re-Preview canaryThe repair root cause is accepted provisionally from the returned Builder result: inbound provider |
Physical read-only re-Preview canary — PASSExact candidate on device: Owner pasted the same phrase batch from the failed production run and tapped Preview exactly once. Observed physical result: This exactly closes the targeted GET-only recovery defect: The prior bad path The Owner did not tap the write/confirm action after this Preview, so this physical canary itself introduced no duplicate phrase POST. Note for the next review: the screenshot also visibly shows the write surface in a |
Clarification — stale Preview concern withdrawnOwner clarified the earlier screenshot showing Current physical screenshot after that fresh Preview shows: Therefore: The physical repair gate is closed. PR #177 remains Draft only because the planned fresh independent PR review is still required before merge. |
New physical evidence after recovery PASS — residual
|
Fresh independent review dispatch checkpoint — 2026-09-06Physical closure before review: Fresh external routing increment: current OpenAI first-party docs position GPT-6 Astra as the most capable model for hard end-to-end reasoning/coding, support up to Sources:
Return Bridge: Review is strictly read-only: no code edits, no PR mutation, no merge, no real Maimemo calls, no Token access, no iPhone Mirroring. Reviewer must return one actual downloadable Review ZIP plus candidate ledger for Coordinator progressive adjudication. |
The Coordinator's accepted repair batch for PR #177: C-02, C-01 and the C-03 test-evidence gap. No new credential, scheduler, executor or recovery stack; nothing outside that batch is touched. C-02 — a scene interruption during a Query run could falsely disconnect. Query owned the one provider operation lane without setting any of the existing lifecycle-busy flags, so `enterBackground()` tore the transient credential down underneath it. The `.active` transition that followed then found `restoreCredentialIfAvailable()` unable to acquire the lane, and ScenePhase promises no second transition, so the app could stay falsely disconnected while active. Query now participates in that same existing coordination: a run holding the lane defers the teardown exactly as an in-flight Preview or authorized write already does, so the credential is simply still there when the scene returns. If the run instead finishes while the app is away, releasing the lane settles the owed teardown, and the next `.active` restores normally. The three duplicated drain bodies collapse onto one `settleDeferredBackgroundTeardown()` helper — one flag, one mechanism, not a second deferred-work queue. Account identity semantics are unchanged. C-01 — a Query 401 never reached the root. The root already had `handleQueryAuthenticationRejection()`, but nothing production-side called it: the store recorded `.globalFailure(.authenticationRejected)` while root `isConnected` stayed true and `beginQueryRead()` could mint another lease from the same rejected credential. `QueryReadLease` — the narrow seam the root already hands Query — gains a once-per-lease `reportAuthenticationRejection()`, and `finishWithGlobalFailure` calls it for `authenticationRejected` only. A decode failure, a transport error and a rate limit stay ordinary read failures. Completed Query truth survives, unfinished cells stay 未读, identity is untouched, and nothing retries. C-03 — `stopDispatching()` cleared `activeTask`, so `awaitRunCompletion()` became a no-op and stop/late-response assertions ran against a signal that answered instantly. The store now also keeps `lastDispatchedRunTask`, which stop does not clear, so awaiting a stopped run really waits for it to unwind and release its lease. It carries no run authority; `runGeneration` alone still decides whether a late response may apply. Tests: 9 new lane/lifecycle tests plus one store test. The C-01 proof runs a real 401 through the production transport, runner and store rather than calling the root handler by hand; the C-02 proofs step a gated transport through both interruption orderings. A negative control that reverted only the three behavioural lines failed 6 of them. focused 47/0, affected 148/0, full unit 421/0, simulator UI 10/0. No real Maimemo call, no real Token read, no physical-device run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… scroll position (#161) Bounded PR #177 repair of three Coordinator-adjudicated findings: - A-01: interpretation Preview and both native write confirmations (single-group and mixed-batch) now state the exact intended 公开/未发布 status, derived from the bound PreviewSnapshot rather than the live publicationPreference, for CREATE-only and mixed plans alike. - B-03: the calm-tier Query duration advisory now says the minutes figure assumes available quota and that recent large batches on the shared 5-hour RequestWindowScheduler allowance can make the real wait longer, instead of reading like a precise ETA. - D-01: QueryView's result list uses native SwiftUI scrollTargetLayout()/scrollPosition(id:anchor:) so ordinary scrolling keeps QuerySessionStore.scrollAnchor current, not only a row tap, so 修改 -> 返回结果 restores the actual reading position. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t target Several CompanionViewModelTests-adjacent test helpers constructed CompanionViewModel without overriding preferenceDefaults, so they shared the app's real .standard UserDefaults domain. Under randomized execution order, one test's WriteTagPreference/InterpretationPublicationPreference write (or leftover state from a prior run) could leak into another test's initial selectedTags/publicationPreference. Give every test-constructed CompanionViewModel its own isolated UserDefaults(suiteName:) domain by default (TestSupport.isolatedPreferenceDefaults()), wired through every connectedModel-style helper and every direct construction across the test target. Add a negative-control test proving the mechanism: two models sharing an explicit suite observe each other's writes; two independently isolated models never do. Enable randomized execution order for the MomoMoreEfficientTests scheme target so this class of bug surfaces going forward (note: xcodebuild CLI did not appear to honor this flag in local verification runs; it is a legitimate Xcode scheme setting regardless). No production source changed. Verified: focused suites green, and the full MomoMoreEfficientTests target green across 5 consecutive runs (425/425, 0 failures, 0 crashes). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ariance Capture Gate run 34130642290 failed both attempts at a flat 10s waitForExistence for the extension's own title after tapping the Share Sheet row. The identical test/build passed one run earlier (34057604549, head d0c427c) with that same title appearing in ~4s, and the only commit between that green head and this one (4dc2a53) is verified test-only (CompanionViewModel preference isolation; no ShareExtension/UI-test/app files touched). Test suite order is identical between both runs, and there is no crash/OOM signal in the failing run's log. This points to extension-process cold-start cost on a loaded shared CI runner, not a broken or structurally unobservable extension UI: the host app's XCUIApplication instance does reliably see the extension's UI when given enough time, which the historical passing runs already prove. Widen the two timeouts governing that observation window (appear after tap, disappear after save) from 10s to 20s. Nothing else changes: same real Share Sheet, same real Share Extension, same exact-payload proof. Verified locally on Xcode 26.6 / iOS 26.5 Simulator: the four focused Capture Gate suites (ShareCaptureTests, CaptureReviewTests, CapturePendingReviewUITests, CaptureShareSheetUITests) all pass, 0 failures, including the full real Share Sheet -> extension -> save -> Capture Review exact-payload path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements Issue #161 from the accepted final Design handoff, following the
Coordinator's v2 Builder contract and the dual-audit adjudication
(comment 5553483948).
Do not merge. A fresh independent review is required first: this changes
real interpretation write and readback semantics.
Retrofit classification
CredentialSession,TokenStore,RequestWindowScheduler,VocabularyTargetResolver,PreflightPlanner,WriteExecutor,PhraseCreateCore,ExecutionHistorystore,CaptureReviewStore,PendingCaptureInboxDomain,ConfirmationBinding,MaimemoTransport,HTTPTransport,ExecutionReceipt,CompanionViewModel,RehearsalModeContentViewsplit into per-route viewsAppRouter,QuerySessionStore,QueryReadRunner, Query core,Theme+ componentsNo second credential stack, scheduler, resolver, History store, workflow
engine, query language or database was introduced.
What changed
Interpretation publication status (
公开/未发布) — a value threadedthrough the existing chain: preference → Preview snapshot → binding context
→ batch/binding digest → fresh authenticated preflight → request body →
authenticated readback → receipt. Not a new approval stack.
makePlan's pin toCompanionConstants.statusbecomes an explicit{PUBLISHED, UNPUBLISHED}allowlist; anything else fails closed before adigest, body or POST exists.
matchesIntendedStategains the status dimension, so same text + same tags更新, never一致.intendedStateMismatch→notVerified, and is never retried.CompanionConstants.statusstays immutable for the phrase/legacy path.ExecutionReceipt.interpretationStatusis optional and never backfilled;a committed golden pre-change archive proves old receipts still decode.
助记 (notes) read —
GET /open/api/v1/notes?voc_id=…on the sameauthenticated transport and shared scheduler, from the current first-party
maimemo/memo-api-clicontract. Closed decoder, no mutation route, recordsreturned whole so Query's counting is a pure projection rather than a change
to shared transport semantics.
Neutral batch Query (read-only) — parser (newline /
,/,only; neverspace,
-,/; no semicolon in v1), advisory-only request budget derivedfrom the real scheduler windows, sequential row-major reads with one request
in flight, atomic resolver stage then atomic per-cell completion, stop /
resume / modify / back, local AND filter, detail from already-returned
objects, clipboard copy of the matched subset. Memory only.
One provider operation lane — the scheduler is a rate ledger, not a
single-flight proof, so credential validation, Preview, writes and Query now
take an explicit lane. Tests observe the fake transport's in-flight
high-water mark, not scheduler reservations.
Two-dimensional account identity — Keychain fingerprint plus an authority
generation advanced only by an explicit successful connect / replace /
remove. A background suspension, a same-Token foreground restore, a failed
candidate and a 401 all leave Query truth alone; a deliberate reconnect after
a rejection clears it even with the same Token.
Shell — 首页乙 / 墨与米 as semantic asset-catalog roles with Any / Dark /
Increased-Contrast variants; Settings owns account management; contextual
History filters one store and
清空历史stays global and says so; Capturekeeps its lifecycle, its
抓词bar and its three accessibility identifiers,and accepting replaces the path so
.writeappears exactly once.Verification
Mirroring, no TestFlight build/version/upload change.
UNPUBLISHED_READBACK_ASSUMPTION=unverified_real_account— the first-partyschema and CLI prove the contract, but actual visibility/readback of an
UNPUBLISHED interpretation under the Owner's token is still not physically
proven. Verification is not weakened anywhere: a missing or mismatched
readback fails rather than being treated as success.
Two pre-existing issues repaired
#176bumpedCURRENT_PROJECT_VERSIONto 4 but left an assertion pinnedat 3, so
BindingAndExecutionTestscould not pass onmain. Re-synced theassertion; no build/version/upload state changed.
小黑鸟伴侣by label alone, which nowalso matches Home's header. Tightened to the hittable row.
For the fresh reviewer
Priority attack surfaces are listed in the final Builder report.
🤖 Generated with Claude Code