From 87f42b626b0cf3c510df54d0ddf9679b7443cf6a Mon Sep 17 00:00:00 2001 From: choiyounggi <74581798+choiyounggi@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:51:52 +0900 Subject: [PATCH 1/2] =?UTF-8?q?knowledge:=20ingest=2012=20verified=20insig?= =?UTF-8?q?ht(s)=20=E2=80=94=207=20new=20pages,=201=20amended,=2077=20plan?= =?UTF-8?q?-gaps=20retired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: testing/mocking/fake-intersection-observer-for-viewport-animations, testing/quality/alias-table-contract-tests, frontend/state/concurrent-optimistic-updates, frontend/design/scrubbed-scroll-animations-under-reduced-motion, infrastructure/config/config-values-emitted-as-source-text, backend/java/kotlin/coerced-enum-defaults-in-kotlinx-serialization, backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions, backend/common/llm/self-hosted-model-load-latency. Amended: infrastructure/agent-orchestration/checkable-claims-in-an-adopted-plan. Indexes: testing, frontend, infrastructure, backend, backend/java, INDEX.md; log.md +9. Report: .dev-loop/INGEST_REPORT.md (lint 361 pages / 0 findings / 0 violations). --- .dev-loop/INGEST_REPORT.md | 300 ++++++++++-------- INDEX.md | 8 +- log.md | 18 ++ .../llm/self-hosted-model-load-latency.md | 71 +++++ wiki/backend/index.md | 1 + wiki/backend/java/index.md | 2 + ...-enum-defaults-in-kotlinx-serialization.md | 65 ++++ ...t-receiver-shadowing-in-scope-functions.md | 65 ++++ ...-scroll-animations-under-reduced-motion.md | 70 ++++ wiki/frontend/index.md | 2 + .../state/concurrent-optimistic-updates.md | 70 ++++ .../checkable-claims-in-an-adopted-plan.md | 10 +- .../config-values-emitted-as-source-text.md | 67 ++++ wiki/infrastructure/index.md | 3 +- wiki/testing/index.md | 2 + ...ection-observer-for-viewport-animations.md | 83 +++++ .../quality/alias-table-contract-tests.md | 69 ++++ 17 files changed, 765 insertions(+), 141 deletions(-) create mode 100644 wiki/backend/common/llm/self-hosted-model-load-latency.md create mode 100644 wiki/backend/java/kotlin/coerced-enum-defaults-in-kotlinx-serialization.md create mode 100644 wiki/backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md create mode 100644 wiki/frontend/design/scrubbed-scroll-animations-under-reduced-motion.md create mode 100644 wiki/frontend/state/concurrent-optimistic-updates.md create mode 100644 wiki/infrastructure/config/config-values-emitted-as-source-text.md create mode 100644 wiki/testing/mocking/fake-intersection-observer-for-viewport-animations.md create mode 100644 wiki/testing/quality/alias-table-contract-tests.md diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md index 939e9fb..506e707 100644 --- a/.dev-loop/INGEST_REPORT.md +++ b/.dev-loop/INGEST_REPORT.md @@ -1,149 +1,181 @@ -# Knowledge ingest — WebMCP as the development standard: 2 pages re-verified, 2 new pages, routing widened +# Knowledge flush — 12 insight(s) + 77 plan-gap rows -Trigger: a Korean WebMCP explainer video (2026-09) pasted for evaluation. Its own content -(declarative vs imperative API, shared page logic, token savings vs browser agents) was already -covered by the two pages ingested 2026-08-18; adoption advocacy and proposal history were again -left out. Verifying the video's claims against primary sources found the wiki a month behind its -sources; the owner then decided (2026-09-28) that the additive WebMCP tool layer is the -development standard for web UI work, QA, and bug fixes, which changes routing. +Flush run id `20260928-082510-51682` (inherited from the auto-flush hook that spawned this session via `DEV_LOOP_FLUSH_RUN_ID`; step-0 acquire returned `already-owned` — a first attempt with a freshly generated id was correctly refused as `held`). Claimed queue ids: 12 session-harvested insights (`132b858e49df7e86`, `06c2127a32c50870`, `b144167a310dd409`, `f3aa894173b74a56`, `bb26106b019128e2`, `5cc5ca049c547407`, `0eaa58be75d0934d`, `21f8661fbab9c65f`, `b7c75b8ce0c9c759`, `1ad754070391a970`, `4dc7dd52cf801076`, `36d3d44f9cfe1f21`) plus the 77 rows of `plan-gaps.jsonl` (all `sessionId: plan-gaps`, harvested 2026-09-27). + +Outcome: **7 new pages, 1 amended page, 5 domain indexes + INDEX.md updated, no new category**; 12 of 12 session candidates handled — 11 ingested into 7 new pages (three rows merged into one page, two rows merged into another), 1 merged into an existing page; 77 plan-gap rows dropped as project-specific (listed under Local-layer candidates). Lint after the edits: `node scripts/wiki-structure-checks.js wiki` → **pages: 361, indexes: 13, findings: 0**; `node scripts/wiki-lint-prohibitions.js wiki` → **directives: 81, compliant: 81, violations: 0, info: 1** (the info line is the pre-existing `keys-ahead-of-their-consumer` cell, present on the untouched baseline: 353 pages / 0 findings / 0 violations / info 1). Largest touched body: `checkable-claims-in-an-adopted-plan` 100 lines; new pages 53–69 lines — all ≤ 120. + +Method: three read-only research agents fetched sources and quoted them; the coordinator then re-checked every URL cited below with `curl -sL -o /dev/null -w '%{http_code}'` (all 200; the Ollama FAQ moved from `faq.md` (404) to `faq.mdx` (200) and the page cites the live path), re-read the framer-motion 13.2.0 `observers.mjs` source itself, re-extracted the AGP `buildConfigField` sentence and the gradle-tips `BUILD_TIME` example from the live HTML, and re-fetched the Ollama FAQ and the TkDodo article. `git status --porcelain` on the checkout was re-read after the agents returned: only the coordinator's own edits were present. ## Verified best-practice -### 1. `consequentialHint` / `debugging` annotations, ChatGPT site-tools constraints, DevTools pane → **verified** - -- https://webmachinelearning.github.io/webmcp/ — Draft Community Group Report dated 2026-09-26; - IDL `partial interface Document { readonly attribute ModelContext modelContext }` (Document - only); `dictionary ToolAnnotations { readOnlyHint, untrustedContentHint, consequentialHint, - debugging }`; no user-confirmation primitive defined. (fetched 2026-09-28) -- https://developer.chrome.com/docs/ai/webmcp/imperative-api — page dated 2026-09-21; - `document.modelContext.registerTool({ name, description, inputSchema, execute, annotations }, - { signal, exposedTo })`; `consequentialHint` "allows agents and browsers to enforce mandatory - user confirmation prompts before executing high-stakes tools"; `debugging` Chrome 156+; no - `requestUserInteraction` mention (the security page's old note on it was removed). -- https://developer.chrome.com/docs/devtools/application/webmcp — page dated 2026-05-12; the - WebMCP pane is in the Application panel; Available Tools (name, description, invocation count), - Invoked Tools (status, input, output), Run tool with manual parameters, schema-mismatch errors - in the output pane. -- https://learn.chatgpt.com/docs/webmcp — "Site tools are ChatGPT's implementation of the - proposed WebMCP standard"; feature-detects `document.modelContext.registerTool`; declarative - API and iframe registrations unsupported; "Each tool invocation receives a safety review before - it runs"; GPT-5.6 Sol / GPT-6 Sol only, Luna disabled; desktop app; not in Enterprise/Edu; - surfaces: built-in browser, ChatGPT Work, Codex; user toggle under Settings → Browser → - Permissions. (help.openai.com's site-tools article was dropped as a source: it returns 403 to - fetchers, so its claims could not be verified.) -- https://developer.chrome.com/docs/ai/webmcp/secure-tools — page dated 2026-09-01; budgets - 30 / 500 / 150 / 1.5K. Contains no auth-state guidance, so the parity gate's both-auth-states - row is derived from security-agent-exposure-in-session-tool-exposure (PII via read tools, - server-side authz unchanged), not from this page. -- https://developer.chrome.com/docs/ai/webmcp — page dated 2026-08-07; origin trial from Chrome - 149; Model Context Tool Inspector extension; prompts go to `gemini-3-flash-preview`. - -### 2. WebMCP-as-standard routing (owner decision) → **policy, not a sourced claim** - -The widened triggers (any new or changed user action in a web UI) and the parity gate's -"every action has a tool unless on the exclusion list" are the owner's development standard, -stated as such in log.md. Every mechanical directive inside those pages is sourced as above. -The standard keeps the existing "human UI primary, tool layer additive" directive unchanged. +**1. `132b858e49df7e86` + `06c2127a32c50870` + `b144167a310dd409` — a fake IntersectionObserver under motion/framer-motion `whileInView`** (→ `confidence: verified`, new page `testing-mocking-fake-intersection-observer-for-viewport-animations`) +Claims: (a) the wrapper caches one observer per (root, `JSON.stringify(options)`) in a module-level WeakMap, so constructor-count probes read 0 from the second test in a file and `observe(element)` is the countable event; (b) the per-element callback is resolved by `observerCallbacks.get(entry.target)`, so a fake entry without `target` makes the animation silently never fire while first-paint styles keep the suite green; (c) a timing suite needs an end-state positive control and a hardcoded-defaults mutation. +Sources checked: `/Users/choeyeong-gi/Desktop/workspace/cover-letter/node_modules/framer-motion/dist/es/motion/features/viewport/observers.mjs` (v13.2.0, read in full — `observers = new WeakMap()`, `key = JSON.stringify(options)`, `fireObserverCallback = (entry) => { const callback = observerCallbacks.get(entry.target); ... }`, `rootInteresectionObserver.observe(element)`); upstream `https://raw.githubusercontent.com/motiondivision/motion/main/packages/framer-motion/src/motion/features/viewport/observers.ts` (200, same logic); `https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/target` (200); `https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/observe` (200); `https://vitest.dev/api/vi.html` (200). No official motion doc on jsdom testing exists (agent checked motion.dev docs; the repo tests `whileInView` under Cypress, not jsdom) — the page does not claim one. +Verification: source read + the session's own reproductions (constructor probe `expected +0 to be 1`; element probe 1 vs mutant 4; target-less fake green for a hardcoded-transition mutant, with `target` 2 red and run time 1 ms → 1018 ms). The third row (`b144…`, the reviewer's restatement with empty domain) carries no separate claim and is folded into the same page. -## Existing-layer check +**2. `f3aa894173b74a56` + `bb26106b019128e2` — concurrent optimistic per-field PATCH updates against one server-owned object** (→ `confidence: verified`, new page `frontend-state-concurrent-optimistic-updates`) +Claim: per-call snapshot + whole-object replace-on-success lets one call's response or rollback overwrite another in-flight call's optimistic field; keep a confirmed snapshot plus an ordered pending-patch overlay, serialize the requests, set `confirmed = response` on success and drop only the failed patch on failure; with a query cache invalidate only when `isMutating() === 1`. +Sources checked: `https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates` (200 — cancels refetches "so they don't overwrite our optimistic update", "multiple mutations running at the same time", links the concurrent-updates guide as further reading); `https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query` (200, TanStack Query maintainer — "If that refetch is faster than our second mutation, our UI will revert…", `if (queryClient.isMutating() === 1) { queryClient.invalidateQueries(...) }`). The Apollo optimistic-UI page was not examined; the page cites only the two above. +Verification: sources + field test (4 overlap tests with `CompletableDeferred` gates red on the snapshot version, green after; auditor reproduced the red; 131/0). The two rows are the same fix seen from the implementer (mobile) and the reviewer (frontend); one page, `applies_to: [general, react, android]`. + +**3. `5cc5ca049c547407` — AGP `buildConfigField` writes the value as Java source** (→ `confidence: verified`, new page `infrastructure-config-config-values-emitted-as-source-text`) +Claim: the value argument is emitted verbatim into `BuildConfig.java`, so user/env input must be escaped (`\` then `"`) and a hostile-character compile probe belongs in the verification step because javac fails before any runtime validation. +Sources checked: `https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/VariantDimension` (200; live text re-extracted: "The field is generated as: = ; This means each of these must have valid Java content. If the type is a String, then the value should include quotes."); `https://developer.android.com/build/gradle-tips` (200; live example `buildConfigField("String", "BUILD_TIME", "\"${minutesSinceEpoch}\"")`). The escaping requirement and the `unclosed string literal` error are consequences of the documented "valid Java content" rule plus the session's javac 17 reproduction (exit 1 vs control exit 0) — the page states them as such. + +**4. `0eaa58be75d0934d` — kotlinx.serialization `coerceInputValues` and enum defaults** (→ `confidence: verified`, new page `backend-java-kotlin-coerced-enum-defaults-in-kotlinx-serialization`) +Claim: with `coerceInputValues = true`, an unknown enum value on a property with a default is silently replaced by the default; a required enum property (no default) still throws. +Sources checked: `https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md` (200; raw text re-read: supported invalid values are "`null` inputs for non-nullable types" and "unknown values for enums"; "If value is missing, it is replaced either with a default property value if it exists, or with a `null` if explicitNulls flag is set to `false` and a property is nullable (for enums)"; the `Brush` example). Field test: adding `= TripPlaceCategory.ETC` made `tripPlace_unknownCategory_throws` fail (9 tests, 1 failed); removing it restored green. + +**5. `21f8661fbab9c65f` — a top-level fixture helper shadowed by a receiver member inside `apply {}`** (→ `confidence: verified`, new page `backend-java-kotlin-implicit-receiver-shadowing-in-scope-functions`) +Claim: inside a lambda with receiver an unqualified call resolves to the implicit receiver's member before a same-named top-level function. +Sources checked: `https://kotlinlang.org/spec/overload-resolution.html` (200; "Call without an explicit receiver": local callables → "overload candidate sets for each pair of implicit receivers … in order of the receiver priority" → "top-level non-extension functions named f"); `https://kotlinlang.org/docs/scope-functions.html` (200; `apply`/`run`/`with` = `this`, `also`/`let` = `it`). No compiler inspection for this shadowing was found (the agent reports "none found", not "confirmed absent"); the page says the member call "compiles cleanly", which the field evidence shows. Field test: 3 failures with the fake's `NoSuchElementException` → rename to `tripFixture` → 13 pass. -Pages read: frontend-agent-interfaces-agent-facing-tool-surfaces, security-agent-exposure-in-session-tool-exposure, qa-process-release-gates, testing-strategy-differential-testing, testing-strategy-cross-layer-effect-tests, testing-strategy-failing-test-first +**6. `b7c75b8ce0c9c759` — an adopt-only plan that freezes test bodies and forbids new failures** (→ `confidence: field-tested` for this addition; merged into the existing `verified` page `infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan`) +Claim: only a run reveals whether the plan's rule flips an existing assertion — apply one production slice, run the whole suite, diff the failing set against the baseline, revert, report the gap. +Sources checked: `http://www.extremeprogramming.org/rules/spike.html` (200 on http only; "A spike solution is a very simple program to explore potential solutions … expect to throw it away") supports the throwaway-probe principle; the compound procedure is field evidence only (seagrass t174: failures 1→2, the new one at `test_repo_policy.py:221` inside a frozen class; reverted, tree clean). The page's `sources:` list is unchanged (the XP page was not added because the addition is recorded as field evidence, not as a sourced directive); the new material is a trigger sentence, one Do-table row, one edge row and one field-evidence line. + +**7. `1ad754070391a970` — first request to a self-hosted model server after idle** (→ `confidence: verified` for the keep-alive mechanics, new page `backend-common-llm-self-hosted-model-load-latency`) +Claim: the first call after idle pays the model's disk load, which exceeds a 10 s SDK default timeout for a multi-GB model; raise the timeout, warm up before measuring, set keep-alive longer than the caller's idle interval. +Sources checked: `https://github.com/ollama/ollama/blob/main/docs/faq.mdx` (200; re-fetched: "By default models are kept in memory for 5 minutes before being unloaded"; `keep_alive` accepts duration strings, seconds, negative numbers, `0`; "The `keep_alive` API parameter … will override the `OLLAMA_KEEP_ALIVE` setting"); `https://github.com/ollama/ollama/blob/main/docs/api.md` (200; `keep_alive` "default: 5m"). The candidate named the server "ollaya" (`OLLAYA_KEEP_ALIVE`) and the client "typesafe-sdk": the research agent found only GitHub repos created 2026-09-17 → 2026-09-27 with copy-pasted descriptions across unrelated accounts and flagged them as not a genuine established project. **The page names neither**; it describes "Ollama or a server with the same keep-alive model" and cites Ollama's docs, and the field evidence (`31334 ms ERROR … Request timed out (timeout=10.0)`; warm-up 1588 ms; p50 786 ms; "4 minutes from now") is recorded brand-free. The load-time-vs-timeout numbers are field evidence, not a documented figure. + +**8. `4dc7dd52cf801076` — row-for-row test of a compatibility alias table** (→ `confidence: field-tested`, new page `testing-quality-alias-table-contract-tests`) +Claim: assert every alias from the design document's mapping with a size assertion and prove detection by retargeting one alias; a 4-of-18 spot check lets wrong forwards through. +Sources checked: `https://pitest.org/quickstart/basic_concepts/` (200; killed mutant = a test failed), `https://testing.googleblog.com/2021/04/mutation-testing.html` (200), `https://junit.org/junit5/docs/current/user-guide/` (200; parameterized tests). These support the mechanics (table-driven + mutation), not the specific alias-table rule, hence field-tested. Field evidence: t1-design-tokens r1 → 20-row table with size check; r2 reviewer retargeted `ButtonMd` and exactly that test failed. + +**9. `36d3d44f9cfe1f21` — a `scrub` ScrollTrigger is outside `globalTimeline.timeScale()`'s reach** (→ `confidence: verified`, new page `frontend-design-scrubbed-scroll-animations-under-reduced-motion`) +Claim: scrub sets the tween's progress from scroll position, so a global time-scale reduced-motion switch never affects it; read the preference where the effect is created, skip the trigger, set the final state. +Sources checked: `https://gsap.com/docs/v3/Plugins/ScrollTrigger/` (200; scrub "Links the progress of the animation directly to the scrollbar so it acts like a scrubber"), `https://gsap.com/docs/v3/GSAP/Timeline/timeScale()/` (200; "Factor that's used to scale time in the animation"), `https://gsap.com/docs/v3/GSAP/gsap.matchMedia()/` (200; "Accessible animations with prefers-reduced-motion" section). The docs do not state the scrub/timeScale interaction in one sentence; it follows from the two definitions, and the page says so ("the engine sets `progress()` from scroll position instead of playing the tween, so a clock factor … has nothing to scale"). Field evidence: cover-letter `DurationMeter.tsx`, caught by the integration reviewer. + +## Existing-layer check -Whole-wiki grep `webmcp|modelContext|toolname|agent-friendly|answer engine` → 7 files (the two -WebMCP pages, their two domain indexes, INDEX.md, log.md, platforms/tools/plugin-mcp-server- -registration which matches only on a modelcontextprotocol URL). skills/, hooks/, agents/, -templates/, AGENTS.md → 0 mentions. qa/testing/debugging indexes → no WebMCP routing line (the -one qa hit is the model-coupled-guidance-aging-detector page, unrelated). `wiki_search` was -unavailable (dev-loop-wiki MCP server failed to connect this session); the category pages were -read directly per the skill's fallback. +Pages read: infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan, infrastructure-agent-orchestration-worker-reported-plan-contradiction, testing-quality-spec-artifact-checks, testing-quality-tests-that-cannot-fail, backend-common-reliability-timeouts-and-retries, backend-common-llm-context-window-budget, frontend-state-client-vs-server-state, testing-quality-mutation-harness-file-custody, testing-quality-value-preserving-refactor-assertions -Merge targets: both existing WebMCP pages were **revised in place** (same trigger, same -directive, newer sources) — no new page for that material. The two new pages have new triggers -(a release gate; a test strategy) that no existing qa/testing page covers: release-gates is the -generic checklist page and is linked, not extended; differential-testing / cross-layer-effect- -tests / failing-test-first are referenced from the testing page's edge cases. +Domain indexes read in full: testing, mobile, frontend, infrastructure, backend, backend/java. `wiki_search` (k=5) was run once per candidate unit (9 queries); no hit described the same trigger for any unit. Top hits considered and why they are not merge targets: +- Unit 1 (fake IntersectionObserver): `testing-e2e-e2e-stability` (entry animations under Playwright auto-wait — E2E, not a jsdom fake), `frontend-data-fetching-infinite-scroll` (production use of IntersectionObserver on a sentinel). New page; `related:` to tests-that-cannot-fail, what-to-mock, async-testing, infinite-scroll. +- Unit 2 (concurrent optimistic updates): `frontend-state-client-vs-server-state` (where server state lives — read in full, no optimistic-update guidance), `databases-selection-relational-jsonb-vs-document-store` (server-side concurrent JSONB writes), `mobile-lifecycle-process-death-and-state`. New page; `related:` to client-vs-server-state, race-conditions, async-ui-states, mobile-offline-first-sync. +- Unit 3 (buildConfigField): `backend-java-kotlin-compiler-daemon-heap-pressure`, `platforms-shells-option-like-argument-values` (text re-parsed as source — the closest analogue, linked as `related:`), `backend-common-change-impact-compiler-as-call-site-inventory`. New page under infrastructure/config (existing category: "per-environment config"); `related:` to environment-config. +- Unit 4 (coerceInputValues): `backend-java-kotlin-frameworks-and-jpa` (Kotlin defaults vs DDL — a different default trap, linked), `testing-quality-schema-additions-under-a-golden-gate` (enum mutation in a golden gate, linked), `backend-java-kotlin-null-safety-interop` (linked). New page under backend/java/kotlin. +- Unit 5 (implicit receiver shadowing): `backend-java-kotlin-null-safety-interop`, `testing-mocking-extracted-method-this-binding` (a JS `this`-binding trap, different mechanism), `testing-quality-value-preserving-refactor-assertions`. New page under backend/java/kotlin; `related:` to test-data-and-isolation, what-to-mock, null-safety-interop. +- Unit 6 (adopt-only plan contradiction): `infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan` (read in full, 94 body lines) and `infrastructure-agent-orchestration-worker-reported-plan-contradiction` (read in full, 64 lines — governs a contradiction a worker has already reported between a design doc and step files, with the same "run it and read the suite" mechanism). The new case is a rule-vs-rule contradiction discovered before implementation, which is the adopting-side page's scope → **merged into checkable-claims** (trigger sentence, Do row, edge row, field-evidence line; 94 → 100 body lines); its index load-when line extended. No conflict with either page. +- Unit 7 (model load latency): `backend-common-llm-context-window-budget` (read in full — repointing at a self-hosted server and the 400 context-window failure; different failure, linked), `backend-common-reliability-timeouts-and-retries` (read — generic timeout/retry policy, linked), `platforms-processes-non-interactive-cli-invocation`. New page under backend/common/llm. +- Unit 8 (alias table test): `testing-quality-spec-artifact-checks` (read in full — coverage vs validity checks on a Markdown/spec artifact; the alias table is a code contract, so a new page linked both ways), `checkable-claims` step 2 (alias-name collisions, linked), `testing-quality-value-preserving-refactor-assertions`. New page under testing/quality. +- Unit 9 (scrub vs timeScale): `frontend-data-fetching-infinite-scroll`, `testing-e2e-e2e-stability`; frontend/design has no motion page and the domain index's route line already names "motion styling". New page under frontend/design; `related:` to effects-usage (scroll/animation-library effects), accessibility-interactive-elements, and the new fake-IntersectionObserver page. -Related links added both ways: qa-process-release-gates ↔ qa-process-agent-tool-parity-gate; -testing-strategy-cross-layer-effect-tests ↔ testing-strategy-agent-tool-shared-handler-tests; -frontend agent-facing-tool-surfaces and security in-session-tool-exposure ↔ both new pages. +Conflicts flagged: none. Related links: the new pages link outward to the existing pages named above; the existing pages' `related:` lists were left unchanged so this PR's footprint on existing pages stays at the one merged page plus index rows — back-links are for the owner to add at review if wanted. ## Open-PR check -`gh pr list --state open` (2026-09-28): one open PR, #223 (knowledge/choiyounggi-20260927-220735, -15 insights). Its file list contains none of the four WebMCP-related pages; the only overlap is -appended log.md entries (union merge). +`gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"` → one open head: **#223 `knowledge/choiyounggi-20260927-220735`** (15 insights: gitignore re-inclusion, union-merge reassembly, hook input fields, silenced-write redirection, jq unicode escape, EPIPE write ordering, fake-server forward-before-reply, shared-helper invariant; 100 plan-gaps retired). Fetched and diffed `origin/main...origin/knowledge/choiyounggi-20260927-220735 -- wiki/ INDEX.md` (53 files): its new pages are `platforms/tools/hook-input-fields-from-the-reference`, `platforms/tools/unicode-escape-in-a-jq-regex`, `testing/mocking/fake-server-forward-before-reply`, `testing/quality/cross-component-invariant-via-shared-helper` and others in platforms/qa/security; its 100 plan-gap rows are a different, earlier set (harvested 2026-09-21 → 2026-09-23). + +| Candidate | Overlapping open head | Verdict | +|-----------|-----------------------|---------| +| Unit 1 fake IntersectionObserver (3 rows) | none — #223's `fake-server-forward-before-reply` is a network fake's reply ordering, not a DOM observer fake | **new** | +| Unit 2 concurrent optimistic updates (2 rows) | none | **new** | +| Unit 3 buildConfigField | none | **new** | +| Unit 4 coerceInputValues | none | **new** | +| Unit 5 implicit receiver shadowing | none | **new** | +| Unit 6 adopt-only plan contradiction | none — #223 amends `qa/process/completion-claims` and `testing/flaky/diagnosing-flaky-tests`, not the adopted-plan pages | **new** (merged into a main page) | +| Unit 7 model load latency | none | **new** | +| Unit 8 alias table test | none — #223's `cross-component-invariant-via-shared-helper` is a shared test helper across components, not an alias mapping | **new** | +| Unit 9 scrub vs timeScale | none | **new** | +| 77 plan-gap rows | none (disjoint hash set from #223's 100) | **drop** — project-specific (below), not pending-duplicate | ## Routing decision -- frontend/agent-interfaces/agent-facing-tool-surfaces — revised (owning artifact: the UI code). -- security/agent-exposure/in-session-tool-exposure — revised (confirmation gating). -- **qa/process/agent-tool-parity-gate** — new page in the existing `process` category beside - release-gates: it is a release-decision checklist for one surface, so it belongs where - release-gates and regression-scope live; no new category. -- **testing/strategy/agent-tool-shared-handler-tests** — new page in the existing `strategy` - category beside test-level-choice / cross-layer-effect-tests: it decides what to test and at - which level for a two-entry-point action; no new category. -- AGENTS.md routing step 7 — one row added (web UI user action → frontend agent-interfaces, - then the qa parity gate); tests/review-routing.bats pin 6 → 7 rows in the same commit. -- INDEX.md frontend / qa / testing route lines and the three domain indexes updated; log.md - gained two ingest entries and two revise entries. - -## Verification - -- `node scripts/wiki-lint-prohibitions.js wiki` → directives 79, violations 0 (pin unchanged). -- `node scripts/wiki-structure-checks.js wiki --layer bundled` → pages 353, findings 0. -- bats: tests/wiki-*.bats + tests/review-routing.bats + tests/orchestrate-review-pass.bats → - 273/273 ok (one earlier `Recall@5` flake re-ran green; baseline on an untouched HEAD worktree - measured the same 0.87). -- Body lines: frontend 110, security 76, qa 70, testing 66 (limit 120). - -## Independent review (before commit) - -- General reviewer (feature-dev:code-reviewer, fresh context): FAIL → 2 major + 4 minor, all - applied: `consequentialHint` scope aligned with the security page's class table; gate edge row - for a vanished runtime (human-UI release not blocked); logout added to the AbortSignal edge - row and the Registration test; step 2/3 of the testing page conditioned on imperative vs - declarative; AGENTS.md step-7 row admits the exclusion list; qa index clause matched to the - page trigger. -- Adversarial fact-checker (fresh context, every source re-fetched): 8/8 targeted claims - confirmed; FAIL on 2 unsupported sentences + 8 imprecisions, all applied: dropped the - `navigator.modelContext` history (no cited source has it); Run tool no longer claimed to write - Invoked Tools (that log is agent↔page); `consequentialHint` quote re-attributed (draft: "client - or agent"; Chrome: "agents and browsers"); secure-tools' stale `requestUserInteraction()` - mention recorded; `readOnlyHint` "requested" → "in its read-only example"; budgets labelled - as Chrome's recommendations applied as limits, parameter names included; cross-origin edge - row now names `allow="tools"` + `exposedTo` + `getTools({ fromOrigins })`; origin trial and - local flag separated; `SubmitEvent.agentInvoked` / `respondWith()` added to the declarative - test directive. - -## CI agent gate (run 36329841491) — blocker refuted, advisories applied - -- Blocker claimed the CG draft has no "client or agent … selectively enforce" language. Ground - truth (`curl -sL https://webmachinelearning.github.io/webmcp/`, 504,537 bytes, tags stripped, - 2026-09-28): the phrase occurs once, in §6 Security considerations under the mitigation for - "Misrepresentation of Intent": "A boolean consequentialHint annotation acts as a signal to the - client or agent that the tool performs a consequential action … This way they can selectively - enforce mandatory user confirmation prompts before executing high-stakes tools". The gate's - fetch read a truncated page. The page now names the section beside the quote. -- Advisory (chromestatus unverifiable from CI): confirmed via the JSON API — stage 150 - desktop/Android 149–156; Firefox and Safari "No signal". The source line now records the API - path. -- Advisory (Run tool vs Invoked Tools): Do 8 no longer implies manual runs are excluded from the - log; it states only what the DevTools page states. -- Advisory (cross-link gap): qa parity gate ↔ backend-common-api-design-agent-tool-granularity - linked both ways, with one sentence placing the parity table as the release-time reading of - that page's design-time capability map. - -## CI agent gate, second run (36330523418) — blocker applied, quote advisory stands - -- Blocker: the frontend page stated "the development standard is an additive WebMCP tool per - action" as unconditional fact under confidence: verified. Applied: the trigger, the frontend - domain description and load-when line, and the INDEX.md frontend row now condition on "this - wiki's development standard (owner decision, log.md 2026-09-28, a policy rather than a sourced - fact)". Routing width is unchanged; the sentence is a policy the wiki declares, not a claim about - the world. -- Advisory (§6 quote unverifiable from CI): the gate's fetch truncates before §6.3.2 and curl is - blocked in its sandbox; it records the quote as unverifiable, not refuted. Ground-truth grep is in - the PR comment; the two sources lines name the section. -- Advisory (duplication with test-level-choice's extract-and-wire edge row): linked both ways and - named in step 1 as the general rule applied to two entry points. +| Insight(s) | Target | Page | Category decision | +|------------|--------|------|-------------------| +| `132b…`, `06c2…`, `b144…` | testing/mocking | `fake-intersection-observer-for-viewport-animations` (new) | existing category — fake/stub decisions | +| `f3aa…`, `bb26…` | frontend/state | `concurrent-optimistic-updates` (new; `applies_to: [general, react, android]`) | existing category; the hazard is in the client state model, so frontend/state owns it and the INDEX.md frontend route line names "web or mobile view models" rather than a duplicate mobile page | +| `5cc5…` | infrastructure/config | `config-values-emitted-as-source-text` (new) | existing category — config injection at build time; mobile has no build category and the rule holds for any codegen step | +| `0eaa…` | backend/java/kotlin | `coerced-enum-defaults-in-kotlinx-serialization` (new) | existing Kotlin-only category (the index routes "Kotlin-only cases" here); applies to Android and Ktor clients alike | +| `21f8…` | backend/java/kotlin | `implicit-receiver-shadowing-in-scope-functions` (new) | existing category — a language resolution rule, with testing pages as `related:` | +| `b7c7…` | infrastructure/agent-orchestration | `checkable-claims-in-an-adopted-plan` (amended) | merge — same trigger family (adopting a plan you did not write), new Do row + edge row | +| `1ad7…` | backend/common/llm | `self-hosted-model-load-latency` (new) | existing category — consuming LLM servers; brand-free wording | +| `4dc7…` | testing/quality | `alias-table-contract-tests` (new) | existing category — test quality/mutation proof | +| `36d3…` | frontend/design | `scrubbed-scroll-animations-under-reduced-motion` (new) | existing category — the frontend route line already lists "motion styling" | + +No new category. INDEX.md route lines extended for frontend, backend, infrastructure and testing so the new triggers are routable from the root. + +## Local-layer candidates + +All 77 `plan-gaps.jsonl` rows are wiki-plan Phase B decisions of the form "Planning tN: deciding X (no owning wiki page)" whose directives name one repository's own files, symbols, values or routes (`TripApi` Retrofit paths, `LinklyRadius` numeric values, `.channel-view__side` CSS, `RFC_ROUTES` registration, `CLEANUP_REAP_TIMEOUT`, `network_security_config.xml` placement, …). Each is a design record for that repo, not a reusable trigger; the previous flushes (#188, #213, #223) retired the same class the same way. Retired as `drop: project-specific plan-gap`. Two rows sit next to general pages this PR adds — `f5966b045f97bab5` (the per-file `FakeIntersectionObserver` stub decision, whose general lesson is now `testing-mocking-fake-intersection-observer-for-viewport-animations`) and `71ab26670152561c` (required enum fields with no default, whose general lesson is `backend-java-kotlin-coerced-enum-defaults-in-kotlinx-serialization`) — the rows themselves remain project records. Targets below use the closest bundled category; run `wiki-ingest` inside the named project to land any of them. + +| Queue id | Project | Decision | Local target | +|----------|---------|----------|--------------| +| `d9055dd9c9c028d8` | cover-letter | Planning t1-motion-stagger: deciding `as` tag dispatch without `any` | `wiki-local/frontend/design/t1-motion-stagger-deciding-as-tag-dispatch-without-any.md` | +| `fd03be655070a0ff` | cover-letter | Planning t1-motion-stagger: deciding `from` preset values | `wiki-local/frontend/design/t1-motion-stagger-deciding-from-preset-values.md` | +| `f5966b045f97bab5` | cover-letter | Planning t1-motion-stagger: deciding Test IntersectionObserver stub | `wiki-local/frontend/design/t1-motion-stagger-deciding-test-intersectionobserver-stub.md` | +| `474303a727bf72b2` | dace | Planning t3-trip-data: deciding HTTP boundary longitude field name | `wiki-local/mobile/networking/t3-trip-data-deciding-http-boundary-longitude-field-name.md` | +| `7bbccf6326ea06d4` | dace | Planning t3-trip-data: deciding `TripApi` Retrofit path/method table | `wiki-local/mobile/networking/t3-trip-data-deciding-tripapi-retrofit-path-method-table.md` | +| `71ab26670152561c` | dace | Planning t3-trip-data: deciding `category` / `mode` field types | `wiki-local/mobile/networking/t3-trip-data-deciding-category-mode-field-types.md` | +| `92a1b76824103c14` | dace | Planning t3-trip-data: deciding Update-request body encoding (`UpdateTripRequest`, `UpdateTripPlaceRequest`, `UpdateItineraryItemRequest`) | `wiki-local/mobile/networking/t3-trip-data-deciding-update-request-body-encoding-updatetriprequest-u.md` | +| `4d2ac156ca9d688b` | dace | Planning t3-trip-data: deciding `TripViewModel` structure | `wiki-local/mobile/networking/t3-trip-data-deciding-tripviewmodel-structure.md` | +| `0ec3ed5e659f62a3` | dace | Planning t3-trip-data: deciding `TripViewModel` draft/form ownership | `wiki-local/mobile/networking/t3-trip-data-deciding-tripviewmodel-draft-form-ownership.md` | +| `32963894c999e03d` | dace | Planning t3-trip-data: deciding `TripItineraryPlanner.applyOptimizedOrder` | `wiki-local/mobile/networking/t3-trip-data-deciding-tripitineraryplanner-applyoptimizedorder.md` | +| `c6d694c43e1adb93` | dace | Planning t3-trip-data: deciding `updateItineraryItem` (16th `TripApi` route) | `wiki-local/mobile/networking/t3-trip-data-deciding-updateitineraryitem-16th-tripapi-route.md` | +| `e4f4db6b5e9f0203` | dace | Planning t3-trip-data: deciding `TripViewModel` test doubles | `wiki-local/mobile/networking/t3-trip-data-deciding-tripviewmodel-test-doubles.md` | +| `6432f4e1b6eaf63f` | dace | Planning t1-design-tokens: deciding LinklyRadius exact values | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklyradius-exact-values.md` | +| `e72e9fbd49d08484` | dace | Planning t1-design-tokens: deciding Color palette source of truth | `wiki-local/mobile/presentation/t1-design-tokens-deciding-color-palette-source-of-truth.md` | +| `65e9b90e50e73d58` | dace | Planning t1-design-tokens: deciding LinklyTextStyle representation | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklytextstyle-representation.md` | +| `d9cf4fd545e20d29` | dace | Planning t1-design-tokens: deciding Legacy typography name to new role mapping | `wiki-local/mobile/presentation/t1-design-tokens-deciding-legacy-typography-name-to-new-role-mapping.md` | +| `804b55b42f7e9eba` | dace | Planning t1-design-tokens: deciding linklyShadow to LinklyElevation forwarding | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklyshadow-to-linklyelevation-forwarding.md` | +| `6c0445bafa4c6bc8` | dace | Planning t1-design-tokens: deciding Legacy LinklyColor alias targets | `wiki-local/mobile/presentation/t1-design-tokens-deciding-legacy-linklycolor-alias-targets.md` | +| `4307412885ab1516` | dace | Planning t1-design-tokens: deciding LinklyEventListCard / LinklyPhotoGridCell scope of change | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklyeventlistcard-linklyphotogridcell-scop.md` | +| `d2169dcb8d26bd41` | dace | Planning t1-design-tokens: deciding LinklyTabBar / LinklyChatBubble structural-redesign ownership | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklytabbar-linklychatbubble-structural-red.md` | +| `5e3efbda6d43b7b9` | seagrass | Planning t176-http-retry-passthrough: deciding `path`-passthrough regression scope | `wiki-local/backend/common/api-design/t176-http-retry-passthrough-deciding-path-passthrough-regression-scope.md` | +| `885247c1b820b2d6` | seagrass | Planning t176-http-retry-passthrough: deciding `breaker`-reaching-the-driver proof | `wiki-local/backend/common/api-design/t176-http-retry-passthrough-deciding-breaker-reaching-the-driver-proof.md` | +| `7dba3e48db50231e` | seagrass | Planning t176-http-retry-passthrough: deciding CHANGELOG entry | `wiki-local/backend/common/api-design/t176-http-retry-passthrough-deciding-changelog-entry.md` | +| `b887a52ab301fa4f` | seagrass | Planning t174-inworkflow-write-state: deciding Order-aware seed rule | `wiki-local/backend/common/api-design/t174-inworkflow-write-state-deciding-order-aware-seed-rule.md` | +| `6534fb12c57590fe` | seagrass | Planning t174-inworkflow-write-state: deciding No `interp.py` change | `wiki-local/backend/common/api-design/t174-inworkflow-write-state-deciding-no-interp-py-change.md` | +| `046386ad11f734ae` | seagrass | Planning t173-openapi-create-as: deciding Where `_response_schema` sources a `create ... as` alias's entity shape | `wiki-local/backend/common/api-design/t173-openapi-create-as-deciding-where-response-schema-sources-a-create.md` | +| `39272f0714d7ecb0` | seagrass | Planning t173-openapi-create-as: deciding Lookup order when a binding name could match both the alias map and the entity map | `wiki-local/backend/common/api-design/t173-openapi-create-as-deciding-lookup-order-when-a-binding-name-could.md` | +| `78525c7bad510519` | dace | Planning t4-notifications-presence: deciding Chat viewing-presence formula | `wiki-local/mobile/networking/t4-notifications-presence-deciding-chat-viewing-presence-formula.md` | +| `dfc98d8efc0f1c7e` | dace | Planning t4-notifications-presence: deciding Chat-tab-active signal delivery in ChatScreen | `wiki-local/mobile/networking/t4-notifications-presence-deciding-chat-tab-active-signal-delivery-in.md` | +| `58a09c1f2e2840bc` | dace | Planning t4-notifications-presence: deciding Ping payload shape | `wiki-local/mobile/networking/t4-notifications-presence-deciding-ping-payload-shape.md` | +| `605d0efe38edbf26` | dace | Planning t4-notifications-presence: deciding Presence emission while disconnected | `wiki-local/mobile/networking/t4-notifications-presence-deciding-presence-emission-while-disconnecte.md` | +| `1dd089d4720913de` | dace | Planning t4-notifications-presence: deciding NotificationPreference/Update DTO field names + bounds | `wiki-local/mobile/networking/t4-notifications-presence-deciding-notificationpreference-update-dto-f.md` | +| `c7c574450273338b` | dace | Planning t4-notifications-presence: deciding PATCH partial-body encoding | `wiki-local/mobile/networking/t4-notifications-presence-deciding-patch-partial-body-encoding.md` | +| `42c6515bc2a60c93` | dace | Planning t4-notifications-presence: deciding Reminder-offset clamping | `wiki-local/mobile/networking/t4-notifications-presence-deciding-reminder-offset-clamping.md` | +| `efdba02159ef416b` | dace | Planning t4-notifications-presence: deciding Reminder-offset UI component | `wiki-local/mobile/networking/t4-notifications-presence-deciding-reminder-offset-ui-component.md` | +| `e7a83bd5532a5f87` | dace | Planning t4-notifications-presence: deciding SettingsViewModel/AppRoot wiring | `wiki-local/mobile/networking/t4-notifications-presence-deciding-settingsviewmodel-approot-wiring.md` | +| `800330f7e8da269d` | seagrass | Planning t179-migrate-silent-noop: deciding How `migrate` locates a candidate row whose scan-time key derivation (`row_key(entity_id, row)`) misses on re- | `wiki-local/backend/common/api-design/t179-migrate-silent-noop-deciding-how-migrate-locates-a-candidate-row.md` | +| `f19196c74d13965b` | seagrass | Planning t179-migrate-silent-noop: deciding What happens when a run scans ≥1 candidate but ends up writing 0 of them, for ANY reason (all resolved via D1/ | `wiki-local/backend/common/api-design/t179-migrate-silent-noop-deciding-what-happens-when-a-run-scans-1-cand.md` | +| `878d659c451b5fb0` | seagrass | Planning t179-migrate-silent-noop: deciding Whether `run_migration`'s success-path return dict may grow a new key (e.g. `"failed"`) to carry the D2/D3 sig | `wiki-local/backend/common/api-design/t179-migrate-silent-noop-deciding-whether-run-migration-s-success-path.md` | +| `dfa0c5396f050af4` | seagrass | Planning t179-migrate-silent-noop: deciding What `docs/migration.md` must state about the new report/exit-code contract (covers R6) | `wiki-local/backend/common/api-design/t179-migrate-silent-noop-deciding-what-docs-migration-md-must-state-ab.md` | +| `bd37a58d7ac48356` | dace | Planning t1-design-tokens: deciding Legacy typography name to new role mapping (all 20 Typography.kt enum entries accounted for: 18 explicit forwa | `wiki-local/mobile/presentation/t1-design-tokens-deciding-legacy-typography-name-to-new-role-mapping-a.md` | +| `2cb5c98088b7a301` | dace | Planning t1-design-tokens: deciding LinklyDuoGradient representation, location, and test strategy | `wiki-local/mobile/presentation/t1-design-tokens-deciding-linklyduogradient-representation-location-an.md` | +| `8680024116d50e24` | dace | Planning t1-design-tokens: deciding Buttons.kt / Inputs.kt / Surfaces.kt scope of change | `wiki-local/mobile/presentation/t1-design-tokens-deciding-buttons-kt-inputs-kt-surfaces-kt-scope-of-ch.md` | +| `efbfa0706d867da3` | cover-letter | Planning t3-experience-motion: deciding Header "typing" mechanism for the code-frame data-file label | `wiki-local/frontend/design/t3-experience-motion-deciding-header-typing-mechanism-for-the-code-fra.md` | +| `48d686795a34b696` | cover-letter | Planning t3-experience-motion: deciding duration.ts signature and month-counting rule | `wiki-local/frontend/design/t3-experience-motion-deciding-duration-ts-signature-and-month-counting.md` | +| `e1db48ea4f707044` | cover-letter | Planning t3-experience-motion: deciding Count-up rendering mechanism | `wiki-local/frontend/design/t3-experience-motion-deciding-count-up-rendering-mechanism.md` | +| `145207ef8dd603b2` | cover-letter | Planning t3-experience-motion: deciding achievements/techStack stagger wiring | `wiki-local/frontend/design/t3-experience-motion-deciding-achievements-techstack-stagger-wiring.md` | +| `2848a10a1f784497` | cover-letter | Planning t3-experience-motion: deciding Final verification floor for this feature | `wiki-local/frontend/design/t3-experience-motion-deciding-final-verification-floor-for-this-featur.md` | +| `9797dcb4361c60c6` | cover-letter | Planning t2-timeline-motion: deciding **General rule (apply everywhere in this plan and to any future change here): `StaggerGroup`/`StaggerItem` for | `wiki-local/frontend/design/t2-timeline-motion-deciding-general-rule-apply-everywhere-in-this-plan.md` | +| `b877422f1b14561a` | cover-letter | Planning t2-timeline-motion: deciding Card slide-in composition | `wiki-local/frontend/design/t2-timeline-motion-deciding-card-slide-in-composition.md` | +| `025f1a9158912f69` | cover-letter | Planning t2-timeline-motion: deciding Marker point-lighting trigger config + production import | `wiki-local/frontend/design/t2-timeline-motion-deciding-marker-point-lighting-trigger-config-produ.md` | +| `1f1ac6e6e2c4f36a` | cover-letter | Planning t2-timeline-motion: deciding Year-header active-emphasis trigger config | `wiki-local/frontend/design/t2-timeline-motion-deciding-year-header-active-emphasis-trigger-config.md` | +| `d64287dabbd1060e` | cover-letter | Planning t2-timeline-motion: deciding Scoped element lookup inside `useGsap`'s callback | `wiki-local/frontend/design/t2-timeline-motion-deciding-scoped-element-lookup-inside-usegsap-s-cal.md` | +| `90f4f001067283fe` | cover-letter | Planning t2-timeline-motion: deciding Marker color+scale swap mechanism | `wiki-local/frontend/design/t2-timeline-motion-deciding-marker-color-scale-swap-mechanism.md` | +| `4647e4ab8121308e` | cover-letter | Planning t2-timeline-motion: deciding Year-header color swap mechanism | `wiki-local/frontend/design/t2-timeline-motion-deciding-year-header-color-swap-mechanism.md` | +| `d20621305ffcbbca` | cover-letter | Planning t2-timeline-motion: deciding Data-attribute + starting-className placement (exact JSX, per attribute) | `wiki-local/frontend/design/t2-timeline-motion-deciding-data-attribute-starting-classname-placemen.md` | +| `7e69b6d5fab0c5db` | cover-letter | Planning t2-timeline-motion: deciding `TimelineItemCard.tsx` tech-chip stagger implementation | `wiki-local/frontend/design/t2-timeline-motion-deciding-timelineitemcard-tsx-tech-chip-stagger-imp.md` | +| `68c498f5d1dd1db0` | cover-letter | Planning t2-timeline-motion: deciding `@/components/motion` test mock shape (pinned, verbatim code in the fenced block below the table) | `wiki-local/frontend/design/t2-timeline-motion-deciding-components-motion-test-mock-shape-pinned-v.md` | +| `c26fecad5c0c4dfe` | dace | Planning t2-dev-env: deciding Debug-only cleartext policy mechanism | `wiki-local/infrastructure/config/t2-dev-env-deciding-debug-only-cleartext-policy-mechanism.md` | +| `2723ba233882356e` | dace | Planning t2-dev-env: deciding Recording the final `AppEnvironment` API summary | `wiki-local/infrastructure/config/t2-dev-env-deciding-recording-the-final-appenvironment-api-summary.md` | +| `9f05d1a613da06c7` | handfish | Planning t2-cmdexec-wait: deciding The reap-bound value and why it is fixed, not derived from `policy.timeout` | `wiki-local/backend/common/reliability/t2-cmdexec-wait-deciding-the-reap-bound-value-and-why-it-is-fixed-not.md` | +| `8e03f7520152a155` | seagrass | Planning t178-emit-with-payload: deciding Grammar surface for `emit`/`publish` | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-grammar-surface-for-emit-publish.md` | +| `dd481d0ccec0139c` | seagrass | Planning t178-emit-with-payload: deciding Duplicate mapped field names | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-duplicate-mapped-field-names.md` | +| `36c365e42c22749d` | seagrass | Planning t178-emit-with-payload: deciding Runtime payload construction | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-runtime-payload-construction.md` | +| `dd9a1de6d841daf4` | seagrass | Planning t178-emit-with-payload: deciding `spec` compatibility | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-spec-compatibility.md` | +| `b7da7c25be38b089` | seagrass | Planning t178-emit-with-payload: deciding RFC-0002 `StepLine` word-cap drift | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-rfc-0002-stepline-word-cap-drift.md` | +| `d2ffe8bb9a03075d` | seagrass | Planning t178-emit-with-payload: deciding RFC-0049 authoring shape | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-rfc-0049-authoring-shape.md` | +| `c46b93213729f22d` | seagrass | Planning t178-emit-with-payload: deciding RFC_ROUTES registration | `wiki-local/backend/common/api-design/t178-emit-with-payload-deciding-rfc-routes-registration.md` | +| `97e35ec0c3f11450` | handfish | Planning t4-fe-layout: deciding Fix for issue #29 suspect 1 (`.channel-view__side` grows with content) | `wiki-local/frontend/design/t4-fe-layout-deciding-fix-for-issue-29-suspect-1-channel-view-side-gro.md` | +| `0cd3fbeb251a807f` | handfish | Planning t4-fe-layout: deciding Fix for issue #29 suspect 2 (`.thread-panel__list` grows instead of scrolling) | `wiki-local/frontend/design/t4-fe-layout-deciding-fix-for-issue-29-suspect-2-thread-panel-list-gro.md` | +| `32d68342d573a02b` | handfish | Planning t4-fe-layout: deciding Bound for issue #27 (`.new-task-modal__project-list` grows with project count) | `wiki-local/frontend/design/t4-fe-layout-deciding-bound-for-issue-27-new-task-modal-project-list-g.md` | +| `7df0d15a106696c6` | handfish | Planning t4-fe-layout: deciding Bound for issue #27 (roster pushed off-screen), scoped so the standalone `RosterPanel` stays untouched | `wiki-local/frontend/design/t4-fe-layout-deciding-bound-for-issue-27-roster-pushed-off-screen-scop.md` | +| `b5e8294d3ee94b9c` | handfish | Planning t4-fe-layout: deciding Whether `.new-task-modal`'s whole-modal `overflow-y: auto` (new-task-modal.css:10-18) stays | `wiki-local/frontend/design/t4-fe-layout-deciding-whether-new-task-modal-s-whole-modal-overflow-y.md` | +| `a35af19defb7a81a` | handfish | Planning t4-fe-layout: deciding Whether any TSX change is needed | `wiki-local/frontend/design/t4-fe-layout-deciding-whether-any-tsx-change-is-needed.md` | +| `7f0ce70b8c79f8d5` | handfish | Planning t4-fe-layout: deciding Baseline command standardized for gate-A / CI parity | `wiki-local/frontend/design/t4-fe-layout-deciding-baseline-command-standardized-for-gate-a-ci-pari.md` | +| `5aad2d91e92d5e4e` | handfish | Planning t4-fe-layout: deciding Where/when R6 (`npm run build` stays green) is verified as its own gate, separate from vitest | `wiki-local/frontend/design/t4-fe-layout-deciding-where-when-r6-npm-run-build-stays-green-is-verif.md` | +| `bc42469f7b56e958` | handfish | Planning t5-agent-toolwrite: deciding PiHarness / OpencodeHarness behaviour | `wiki-local/infrastructure/agent-orchestration/t5-agent-toolwrite-deciding-piharness-opencodeharness-behaviour.md` | diff --git a/INDEX.md b/INDEX.md index e947472..d86bf4c 100644 --- a/INDEX.md +++ b/INDEX.md @@ -12,10 +12,10 @@ follow the cross-pointers in their index or take the next matching seeded domain | Domain | Status | Route here when | |--------|--------|-----------------| | [databases](wiki/databases/index.md) | **seeded** | Choosing a datastore/database type for a workload (relational vs document vs vector vs graph), designing schemas/tables/keys, choosing or evaluating indexes, writing or optimizing queries, choosing transaction/isolation behavior, multi-row rewrites (reorder, bulk status) on a shared resource, surveying live data to derive a rule, verifying additive migrations | -| [backend](wiki/backend/index.md) | **seeded** | Server-side application code — language-agnostic (`common/`: API contracts, call-site enumeration before a contract change, idempotency, JWT, timeouts/retries, caching, jobs, transactions in app code, shared state/pools, errors, consuming LLM APIs (completion validation, context budgeting), authoring agent-facing artifacts (binding instruction text, agent tool-surface granularity/parity), MAPE-aligned point-prediction calibration, benchmark-relative signal rating, consuming external-API responses, externally-owned defaults, object-storage references, sync-vs-async integration choice, WebSocket/SSE connection lifecycle) plus stack subtrees: `java/` (JPA, Spring proxies, JVM threads/memory), `node/` (event loop, promises, runtime validation, shutdown), `python/` (GIL/asyncio, pydantic, WSGI/ASGI workers, language traps, packaging data files with `importlib.resources`) | -| [frontend](wiki/frontend/index.md) | **seeded** | Web UI code: state placement, rendering performance, in-UI data fetching (races, infinite scroll), auth token handling, forms, XSS-safe output, accessibility, any new or changed user action (this wiki's development standard adds a WebMCP tool surface per action) | -| [infrastructure](wiki/infrastructure/index.md) | **seeded** | CI/CD pipelines, secrets in build/deploy, container image builds, rollout/rollback strategy, observability (logs/metrics/alerting), per-environment/path-valued config, multi-agent orchestration (worker liveness signals, shared run state, tmux pane delivery, completion gates, worktree-isolated workers, autonomous ask-vs-rule decisions, session context/token budgeting, a pre-built code knowledge graph as a freshness-gated orientation layer for planning, the merged-tree gate for parallel branches, the verify command written into a worker brief, a lock owner id inherited from the parent that spawned the session) | -| [testing](wiki/testing/index.md) | **seeded** | Writing or structuring automated tests: level choice, test-before-code ordering, a UI action that is also a registered agent tool, cases/assertions, cross-layer effect scoping, test data, mock decisions, flaky tests, test-infrastructure containers (Testcontainers) failing on the dev host, testing a SwiftPM executable target (release-process quality → qa) | +| [backend](wiki/backend/index.md) | **seeded** | Server-side application code — language-agnostic (`common/`: API contracts, call-site enumeration before a contract change, idempotency, JWT, timeouts/retries, caching, jobs, transactions in app code, shared state/pools, errors, consuming LLM APIs (completion validation, context budgeting, first-call load latency of a self-hosted model server), authoring agent-facing artifacts (binding instruction text, agent tool-surface granularity/parity), MAPE-aligned point-prediction calibration, benchmark-relative signal rating, consuming external-API responses, externally-owned defaults, object-storage references, sync-vs-async integration choice, WebSocket/SSE connection lifecycle) plus stack subtrees: `java/` (JPA, Spring proxies, JVM threads/memory, Kotlin: kotlinx.serialization enum coercion, implicit-receiver shadowing in scope functions), `node/` (event loop, promises, runtime validation, shutdown), `python/` (GIL/asyncio, pydantic, WSGI/ASGI workers, language traps, packaging data files with `importlib.resources`) | +| [frontend](wiki/frontend/index.md) | **seeded** | Web UI code: state placement, rendering performance, in-UI data fetching (races, infinite scroll), auth token handling, forms, XSS-safe output, accessibility, concurrent optimistic updates against one server-owned object (web or mobile view models), scroll-scrubbed animations under a reduced-motion preference, any new or changed user action (this wiki's development standard adds a WebMCP tool surface per action) | +| [infrastructure](wiki/infrastructure/index.md) | **seeded** | CI/CD pipelines, secrets in build/deploy, container image builds, rollout/rollback strategy, observability (logs/metrics/alerting), per-environment/path-valued config, config values a build step writes into generated source, multi-agent orchestration (worker liveness signals, shared run state, tmux pane delivery, completion gates, worktree-isolated workers, autonomous ask-vs-rule decisions, session context/token budgeting, a pre-built code knowledge graph as a freshness-gated orientation layer for planning, the merged-tree gate for parallel branches, the verify command written into a worker brief, a lock owner id inherited from the parent that spawned the session) | +| [testing](wiki/testing/index.md) | **seeded** | Writing or structuring automated tests: level choice, test-before-code ordering, a UI action that is also a registered agent tool, cases/assertions, cross-layer effect scoping, test data, mock decisions (including a fake `IntersectionObserver` under a viewport-animation library), contract tests for a deprecated-alias table, flaky tests, test-infrastructure containers (Testcontainers) failing on the dev host, testing a SwiftPM executable target (release-process quality → qa) | | [qa](wiki/qa/index.md) | **seeded** | Release-quality process: release gates, regression scoping, bug reports, severity/priority triage, evidence for completion claims, the agent-tool parity gate for a web UI release, acting on code-review feedback, adversarial review of high-risk diffs, exploratory testing (guarded-path coverage, override matrices), scope-purity gates, sourcing deliverable documents from generated artifacts, verifying the quantitative claims in a document before publishing it, a documented claim about a third-party tool's side effects, an obligation row in a tier/policy table that another contract also pins, rationale prose left behind by a config-value change, automated verification of document deliverables (spec/RFC gates), an aging detector for model-coupled agent guidance, capturing an app's own screen content without Screen Recording permission (writing automated test code → testing) | | [debugging](wiki/debugging/index.md) | **seeded** | Diagnosing a failure — finding what is wrong and why: reproducing, bisection, hypothesis testing, traces/logs, intermittent failures (fixing the diagnosed fault → its owning domain) | | [security](wiki/security/index.md) | **seeded** | Trust-boundary decisions: input validation, session-vs-token auth choice, per-resource authorization (IDOR), secrets hygiene (including ciphertext orphaned by a regenerated encryption key), dependency trust, PII handling, in-session agent tool exposure (prompt-injection blast radius), the author identity a commit publishes to a public repository, host-compromise triage / incident response (verifying assumed security agents, identifying masquerading processes) (XSS rendering → frontend; CI secrets → infrastructure; JWT implementation → backend/frontend auth) | diff --git a/log.md b/log.md index 72b6053..ee6f721 100644 --- a/log.md +++ b/log.md @@ -208,3 +208,21 @@ Append-only. Format: `## [YYYY-MM-DD] R` lambdas | `this` | The receiver's member `trip` when one exists, else the top-level function | +| `also`, `let` | none (`it`) | The top-level function | + +## Edge cases + +| Case | Then | +|------|------| +| The receiver member is `suspend` and the caller is not in a coroutine | The compiler reports the suspend-call error, which is the first visible symptom — read it as shadowing, not as a missing `runBlocking` | +| Nested receivers (`a.apply { b.apply { trip() } }`) | The innermost receiver wins, then outer receivers, then top-level — the spec orders implicit receivers by priority before top-level callables | +| An extension function shares a member's name | The member wins on an explicit receiver too; rename the extension | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Call a top-level `trip("t1")` inside `FakeApi().apply { }` | Rename the helper or switch to `also { }` | The fake's member `trip` is found first and compiles cleanly | +| Fix the runtime error by seeding the fake with the id the helper asked for | Check which `trip` the call resolves to | The fake was never meant to be called; the data fix hides the wrong wiring | + +## Sources + +- https://kotlinlang.org/spec/overload-resolution.html — "Call without an explicit receiver": for an identifier `f` the sets are analyzed in order: local callables, then "the overload candidate sets for each pair of implicit receivers … in order of the receiver priority", then "top-level non-extension functions named `f`"; the first non-empty set wins +- https://kotlinlang.org/docs/scope-functions.html — `apply`/`run`/`with` expose the context object as `this`; `also`/`let` expose it as `it` +- Field evidence 2026-09-27 (linkly-calendar Android, `TripViewModelTest`): three cases failed with the fake's `NoSuchElementException("no trip t1")` thrown from inside `apply {}`; the top-level `trip(id)` fixture had been shadowed by `FakeTripApi.trip(id)`. Renaming the helper to `tripFixture` made all 13 pass diff --git a/wiki/frontend/design/scrubbed-scroll-animations-under-reduced-motion.md b/wiki/frontend/design/scrubbed-scroll-animations-under-reduced-motion.md new file mode 100644 index 0000000..a9dec5f --- /dev/null +++ b/wiki/frontend/design/scrubbed-scroll-animations-under-reduced-motion.md @@ -0,0 +1,70 @@ +--- +id: frontend-design-scrubbed-scroll-animations-under-reduced-motion +domain: frontend +category: design +applies_to: [general, react] +confidence: verified +sources: + - https://gsap.com/docs/v3/Plugins/ScrollTrigger/ + - https://gsap.com/docs/v3/GSAP/Timeline/timeScale()/ + - https://gsap.com/docs/v3/GSAP/gsap.matchMedia()/ +last_verified: 2026-09-28 +related: [frontend-state-effects-usage, frontend-accessibility-interactive-elements, testing-mocking-fake-intersection-observer-for-viewport-animations] +--- + +# Scroll-Scrubbed Animations Under a Reduced-Motion Preference + +## When this applies + +A codebase honours `prefers-reduced-motion` through one global switch on the +animation engine's clock — `gsap.globalTimeline.timeScale(...)`, a global +duration multiplier, a "skip to end" on played tweens — and you are adding or +reviewing an effect whose progress is driven by scroll position: a GSAP tween +with `scrollTrigger: { scrub }`, a count-up or typing effect updated from a +scroll progress value, a parallax layer. + +## Do this + +1. **Classify each effect as played or scrubbed.** `scrub` "links the progress + of the animation directly to the scrollbar so it acts like a scrubber": the + engine sets `progress()` from scroll position instead of playing the tween, + so a clock factor (`timeScale`, which scales "time in the animation") has + nothing to scale. A global clock switch covers only the played class. +2. **For every scrubbed effect, read the preference where the effect is + created** — `gsap.matchMedia()` with a `"(prefers-reduced-motion: reduce)"` + condition, or `window.matchMedia` — and in the reduced branch skip creating + the ScrollTrigger and set the final state directly (`gsap.set`, final text, + final class). +3. **At review, list every `scrub` and every progress-driven `onUpdate` in the + diff against the codebase's reduced-motion mechanism**; each one needs its + own branch or a documented reason it is exempt. +4. **Test the reduced branch:** stub `matchMedia` to report `reduce`, render, + and assert the final state is present and no trigger was created. + +| Effect | Reduced-motion handling | +|--------|-------------------------| +| Played tween (`gsap.to`, `whileInView`) | The global clock switch or `duration: 0` in the `matchMedia` branch | +| `scrollTrigger: { scrub: true \| 1 }` | Do not create the trigger; `gsap.set` the end values | +| `onUpdate` writing text or attributes from progress (count-up, typing) | Write the final text once in the reduced branch | + +## Edge cases + +| Case | Then | +|------|------| +| `scrub: 1` (smoothed) | Still scroll-driven; the smoothing is a catch-up tween on progress, not playback under the global clock | +| The effect combines a pinned ScrollTrigger with a played child timeline | Split: the pin and scrub stay layout, the child timeline follows the played-class rule | +| The preference changes while the page is open | `gsap.matchMedia()` re-runs the matching context and reverts the other; a hand-rolled `window.matchMedia` check needs a `change` listener | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Rely on `globalTimeline.timeScale(0)` for a scrubbed tween | Branch on the preference and set the final state | Scrub sets progress from scroll; the clock never runs | +| Ship the scrubbed effect and let the integration reviewer catch it | Enumerate `scrub` sites at task review | The seam is per effect, so it is missed by a global check | + +## Sources + +- https://gsap.com/docs/v3/Plugins/ScrollTrigger/ — `scrub`: "Links the progress of the animation directly to the scrollbar so it acts like a scrubber"; `scrub: true` "links the animation's progress directly to the ScrollTrigger's progress" +- https://gsap.com/docs/v3/GSAP/Timeline/timeScale()/ — "Factor that's used to scale time in the animation where 1 = normal speed (the default), 0.5 = half speed, 2 = double speed" +- https://gsap.com/docs/v3/GSAP/gsap.matchMedia()/ — section "Accessible animations with prefers-reduced-motion": `reduceMotion: "(prefers-reduced-motion: reduce)"` condition and `duration: reduceMotion ? 0 : 2` +- Field evidence 2026-09-27 (cover-letter, t3-experience-motion, `DurationMeter.tsx`): the repo's reduced-motion story was `globalTimeline.timeScale()`; a scrubbed count-up meter kept animating under the preference and was caught by the integration reviewer, not at task review. Reworked to read the preference in the component, skip the trigger, and render the final value diff --git a/wiki/frontend/index.md b/wiki/frontend/index.md index 3088f79..bce2abc 100644 --- a/wiki/frontend/index.md +++ b/wiki/frontend/index.md @@ -17,6 +17,7 @@ Match your situation to a "load when" line; load only matching pages. | Page | Load when | |------|-----------| | [client-vs-server-state](state/client-vs-server-state.md) | Deciding where/how to store a piece of UI data (fetched entities vs ephemeral UI vs theme/session vs filters/tabs); untangling a global store that has grown unmanageable | +| [concurrent-optimistic-updates](state/concurrent-optimistic-updates.md) | A view model or store applies optimistic updates to one server-owned object through per-field PATCH calls the UI can fire concurrently (several switches, each its own request); the current shape is snapshot-previous / replace-on-success / restore-on-failure; a toggle reverts when another toggle's response or rollback lands; deciding how to model confirmed vs pending state and when to invalidate a query cache (web or mobile view models) | | [derived-state](state/derived-state.md) | About to store a value computable from existing state/props (filtered list, count, selected object); two copies of the same fact have drifted; tempted to set state from an effect | | [effects-usage](state/effects-usage.md) | Writing or reviewing a useEffect (or framework-equivalent watcher); an effect chain causes render loops, flicker, or double-firing; deciding where non-render logic belongs (event handler vs effect vs module scope); a canvas/WebGL/rAF setup-teardown effect is keyed on an object a hook returns (theme, colors, viewport) that gets a fresh identity on every DOM mutation a scroll/animation library makes (e.g. Lenis toggling `` classes on scroll start/stop), re-running setup though nothing the loop needs changed | @@ -90,4 +91,5 @@ Match your situation to a "load when" line; load only matching pages. | [multi-shape-canvas-mask](design/multi-shape-canvas-mask.md) | Masking or clipping canvas content to the union of several shapes with `globalCompositeOperation = 'destination-in'`; painted strokes vanish after a per-shape mask loop; reviewing a loop that applies `destination-in` once per shape | | [lightness-steps-on-dark-surfaces](design/lightness-steps-on-dark-surfaces.md) | Designing or reviewing dark-UI fill tokens where states or elevation levels differ by OKLCH lightness steps and any step sits below about L 30%; a token ladder is called "perceptually distinct" because its L values differ; deciding whether a state cue meets WCAG 1.4.11's 3:1 or must move to outline/chroma/shape; validating an OKLCH→sRGB converter against the browser | | [pointer-attracted-particle-fields](design/pointer-attracted-particle-fields.md) | Writing or reviewing a canvas/WebGL particle or network background whose nodes move a fixed fraction toward the pointer each frame; the effect clumps after the mouse rests or the page is scrolled; a plan says "pull N % per frame toward the cursor" with no release or inner radius; choosing the regression test for such an update rule | +| [scrubbed-scroll-animations-under-reduced-motion](design/scrubbed-scroll-animations-under-reduced-motion.md) | Adding or reviewing a scroll-driven effect (GSAP `scrollTrigger: { scrub }`, a count-up/typing/parallax updated from scroll progress) in a codebase whose reduced-motion handling is one global clock switch (`globalTimeline.timeScale()`, a duration multiplier); a scrubbed effect keeps animating under `prefers-reduced-motion: reduce` | | [custom-property-values-read-from-script](design/custom-property-values-read-from-script.md) | A script helper reads a CSS custom property with `getComputedStyle(...).getPropertyValue('--x')` to feed canvas `fillStyle`, a chart, or WebGL, and the token set has alias tokens (`--a: var(--b)`); such a helper is tested under jsdom (Jest/Vitest); a canvas draws black although the token is defined; a jsdom test is green while alias tokens misrender; choosing between an empty-or-`var(` fallback guard and a bounded alias-chain resolver, and the alias test cases (single, chained, undefined target, cycle) | diff --git a/wiki/frontend/state/concurrent-optimistic-updates.md b/wiki/frontend/state/concurrent-optimistic-updates.md new file mode 100644 index 0000000..129e686 --- /dev/null +++ b/wiki/frontend/state/concurrent-optimistic-updates.md @@ -0,0 +1,70 @@ +--- +id: frontend-state-concurrent-optimistic-updates +domain: frontend +category: state +applies_to: [general, react, android] +confidence: verified +sources: + - https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates + - https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query +last_verified: 2026-09-28 +related: [frontend-state-client-vs-server-state, frontend-data-fetching-race-conditions, frontend-data-fetching-async-ui-states, mobile-offline-offline-first-sync] +--- + +# Several Optimistic Updates In Flight Against One Server-Owned Object + +## When this applies + +A view model or store applies optimistic updates to one server-owned object +(a preferences record, a profile, a settings row) through per-field PATCH calls +the UI can fire concurrently — several switches, each launching its own request +— and the naive shape is "snapshot previous → replace the whole object with +the response on success → restore the snapshot on failure". Web or mobile; the +hazard is in the state model, not the platform. + +## Do this + +1. **Keep two things, not one:** the last server-confirmed snapshot and an + ordered list of pending patches. Render `confirmed` with the pending patches + folded on top, in order. +2. **Serialize the requests** through a FIFO queue or mutex so each response + is authoritative for everything sent before it. +3. **On success set `confirmed = response`** (the server's full object is now the + truth for all earlier patches) **and remove that patch**; **on failure remove + only that patch** and surface the error — other pending patches stay visible. +4. **With a query cache (TanStack Query / SWR):** cancel outgoing refetches in + `onMutate` and invalidate in `onSettled` only when no other mutation is in + flight (`queryClient.isMutating() === 1`), so an early refetch does not + revert a later mutation's optimistic value. +5. **Write the overlap test before the fix:** two toggles, response gates you + release by hand (a `CompletableDeferred` / deferred promise per call), one + succeeds and one fails, assert the surviving state of both fields. Per-field + tests never exercise the overlap, so keep them and add this one. + +| Situation | Do | +|-----------|----| +| One field toggled repeatedly | Serialize; the last response wins and carries every earlier toggle | +| Different fields patched concurrently | Overlay: each success confirms all earlier fields, each failure drops only its own patch | +| Whole-object PUT instead of PATCH | Build the PUT body from `confirmed` plus every pending patch, still serialized — a body built from the rendered state alone re-sends a failed patch | + +## Edge cases + +| Case | Then | +|------|------| +| The server response includes changes made elsewhere (another device) | `confirmed = response` absorbs them; pending patches still render on top until their own response | +| A patch fails after a later one succeeded | Drop the failed patch; the later success already confirmed the server truth for its own field, so the rendered state converges without a rollback | +| The device is offline | Route through an outbox with conflict rules ([mobile-offline-offline-first-sync]) rather than holding patches in memory | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Restore a per-call `previous` snapshot on failure | Remove only that call's patch from the pending list | The snapshot predates other in-flight patches and reverts them | +| Replace the whole object with each response as it arrives | Serialize the calls, then `confirmed = response` | An earlier call's late response overwrites a later call's optimistic field | +| Invalidate the query on every `onSettled` | Invalidate only when `isMutating() === 1` | The first invalidation's refetch lands while another mutation is in flight and reverts its optimistic value | + +## Sources + +- https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates — `onMutate` cancels outgoing refetches "so they don't overwrite our optimistic update"; "there might be multiple mutations running at the same time"; links the concurrent-updates guide below as further reading +- https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query — TanStack Query maintainer: "If that refetch is faster than our second mutation, our UI will revert and we'll see the dreaded window of inconsistency again"; remedy `if (queryClient.isMutating() === 1) { queryClient.invalidateQueries(...) }` in `onSettled` +- Field evidence 2026-09-27 (linkly-calendar Android, `SettingsViewModel.patchPreference`, Kotlin coroutines): a review lens on execution-environment reality found per-call snapshot + whole-object replace; four overlap tests with `CompletableDeferred` response gates and `runCurrent` were red on that version and green after moving to confirmed snapshot + `pendingPatches` + `Mutex`; the auditor reproduced the red independently; suite 131 passed, 0 failed diff --git a/wiki/infrastructure/agent-orchestration/checkable-claims-in-an-adopted-plan.md b/wiki/infrastructure/agent-orchestration/checkable-claims-in-an-adopted-plan.md index 21162be..e2e037c 100644 --- a/wiki/infrastructure/agent-orchestration/checkable-claims-in-an-adopted-plan.md +++ b/wiki/infrastructure/agent-orchestration/checkable-claims-in-an-adopted-plan.md @@ -7,7 +7,7 @@ confidence: verified sources: - https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html - https://git-scm.com/docs/git-check-ignore -last_verified: 2026-09-04 +last_verified: 2026-09-28 related: [qa-deliverables-quantitative-claims-in-a-published-document, qa-document-verification-spec-document-gates, infrastructure-agent-orchestration-autonomous-decision-rulings, infrastructure-agent-orchestration-unattended-worker-questions, infrastructure-agent-orchestration-worker-reported-plan-contradiction] --- @@ -25,7 +25,10 @@ Also applies when the plan lists a multi-task "Task order / Depends on" table together with each task's own Steps or Inputs section, before implementing any task in it. Also applies when you are the plan's author, about to state a code wiring (a value threaded between two call sites, a lookup keyed by an id built -elsewhere) or a pre-computed number (a contrast ratio) as fact. +elsewhere) or a pre-computed number (a contrast ratio) as fact. Also applies +when an adopt-only plan states two execution rules whose joint satisfiability +depends on the current suite ("do not modify existing test bodies" together +with "stop on any new failure"). ## Do this @@ -73,6 +76,7 @@ elsewhere) or a pre-computed number (a contrast ratio) as fact. | A deliverable is gitignored or a decision has no enactment | Escalate as a plan defect; it blocks every consumer, not only you | | A task's Steps prose names another task's not-yet-built symbol in a direction the dependency table contradicts | Escalate as a plan defect with both readings attached; do not implement in the table's order until the plan owner rules | | A plan's structural premise (a manifest field's shape, a source type) was copied from a sibling repo's or task's plan rather than read from this target | Read the target's real manifest field once before dispatch; escalate if it differs from the copied premise instead of implementing a gate that would compare nothing | +| The plan freezes existing test bodies and also forbids new failures | Apply one production slice, run the whole suite, diff the failing set against the pre-change baseline, revert the probe, and report the delta as a plan defect — reading the plan cannot show whether its rule flips an existing assertion; only a run can | ## Edge cases @@ -83,6 +87,7 @@ elsewhere) or a pre-computed number (a contrast ratio) as fact. | The plan pre-approves silent correction of arithmetic | Correct it, cite the pre-approval in the ruling, and still report the original value alongside | | You are the plan's author and a step threads a value from one call site to another, or keys a lookup by an id built elsewhere (`sink[step_id]` read against a `step["id"]` written as `step_id + ".net"`) | Grep both ends before dispatch — the write site and the read site — and paste both expressions into the plan so the adopter checks a match rather than a claim; confirm too that the call path meant to carry a new argument reaches the target (a second entry point such as `_do_post → _respond` that bypasses the wrapper leaves the parameter unset). A mismatched key or an unreached path raises nothing — a default value, an unset field — so the plan reads as correct until an output is subtly wrong; have the implementer re-run the same greps against source before writing code | | You are the plan's author fixing design-token values (a palette) and pre-computing WCAG contrast for the plan | Open the contract test file and enumerate every (foreground role, background surface) pair it asserts — a role is checked on more surfaces than the visible ones (`muted` on `surfaceSoft` as well as on canvas and card) — then compute all of them; a pair the plan skipped comes back one round later as a worker's plan-gap report or a red contract test | +| The only way to test a rule's compatibility is a probe that touches production code | Probe production code only, never the frozen tests; revert the probe (`git checkout -- `) and confirm `git status --porcelain` is empty before reporting, so the gap report is not itself a forbidden edit | | You are the plan's author, about to write "reuse existing component X" or "token Y supports Z" | Grep the implementation and its consumer count before writing the sentence, and record the command beside the claim (`grep -rn \| wc -l`) so the adopter's check is a re-run rather than a discovery — a file named for the concept can implement something else (a "modal" file that is a bottom sheet with a grabber and one consumer) or have zero consumers | ## Instead of @@ -103,4 +108,5 @@ elsewhere) or a pre-computed number (a contrast ratio) as fact. - Field evidence 2026-08-23 (dev-loop mpa1 orchestration run, dl-version-gate task): a coordinator wrote the version-gate plan for dev-loop by copying a sibling task's plan for a different repo, carrying over a local-path source pairing. dev-loop's own marketplace entry is a `url`-source self-reference, not a local path, so the copied pairing would have compared nothing and the gate would always pass. The worker's adopt-or-report gate flagged the premise instead of implementing it; the coordinator re-planned to a name-match pairing (r2), approved on 6 green bats cases — a full re-plan round that one `jq` read of the real `source` field would have avoided - Field evidence 2026-08-26 (linkly, plan authoring): two silent-failure wirings caught by grepping both ends before dispatch — `verb_sink[step_id]` read against a `step["id"]` written as `step_id + ".net"` (lower.py:2148 vs 1138), and `_respond` reachable through `_do_post` without the JSON-log wrapper meant to pass its new argument (wsgi.py:818-830); neither path would have raised, and the implementer's re-verification against source was the second catch - Field evidence 2026-08-30 (linkly-calendar t1, `TokenContractTests.swift`): the coordinator pre-computed contrast for canvas and card backgrounds only; the contract test also checks `muted` on `surfaceSoft`, which measured 4.41:1 (below the 4.5:1 AA floor) and forced a palette re-adjustment round recorded in `.orchestration/notes/decisions.md` +- Field evidence 2026-09-27 (seagrass, t174-inworkflow-write-state, adopt-only plan): the plan froze existing test bodies and required a stop on any new failure; replacing only the `repo_policy.seeded_entities` body with the plan's order-aware rule and running the whole suite moved failures from 1 to 2, the new one at `test_repo_policy.py:221` inside a class the plan froze. The probe was reverted (`git checkout --`, tree clean) and the contradiction reported as a plan gap before any implementation - Field evidence 2026-08-31 (linkly-calendar iOS design review): `LinklyModal.swift`, named in the draft as a reusable modal, was a bottom sheet (`grabber` + `.rect(topLeadingRadius:)`) with one consumer, and `LinklyCalendarRangeLozenge` had zero consumers by `grep -rn`; the design that planned to reuse both was corrected before implementation, and the grep commands were kept in the document diff --git a/wiki/infrastructure/config/config-values-emitted-as-source-text.md b/wiki/infrastructure/config/config-values-emitted-as-source-text.md new file mode 100644 index 0000000..1aea40e --- /dev/null +++ b/wiki/infrastructure/config/config-values-emitted-as-source-text.md @@ -0,0 +1,67 @@ +--- +id: infrastructure-config-config-values-emitted-as-source-text +domain: infrastructure +category: config +applies_to: [general, android] +confidence: verified +sources: + - https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/VariantDimension + - https://developer.android.com/build/gradle-tips +last_verified: 2026-09-28 +related: [infrastructure-config-environment-config, platforms-shells-option-like-argument-values] +--- + +# A Build Step That Writes a Config Value Into Generated Source + +## When this applies + +A build tool takes a configuration value from user or environment input (a +Gradle `-P` property, an env var, a CI secret) and writes it into generated +source code that a compiler then parses — Android Gradle Plugin +`buildConfigField(type, name, value)` into `BuildConfig.java`, a resource or +constants file emitted by a codegen step — and the design says "validation +happens at runtime". + +## Do this + +1. **Treat the value as a literal in the target language, not as data.** AGP + documents `buildConfigField` as generating ` = ;` where + each part "must have valid Java content" and a `String` value "should include + quotes". Escape `\` first and then `"` inside the input, wrap the result in + quotes, and reject or encode newlines. +2. **Put the escaping in one helper** used by every emitted field, so a new + field cannot bypass it. +3. **Add a build-time probe to the verification step:** run the build + (`assembleDebug`, or the codegen plus its compiler) once with an input that + contains `"` and `\`. The failure this guards against is a compile error + (`unclosed string literal`), which happens before any runtime check runs. +4. **Keep the runtime validation** (URL parse, allow-list) — it covers values + that compile but are wrong — and state in the design that it only runs on a + build that compiled. + +| Field type | Emit | +|------------|------| +| `String` | `"\"" + escaped(value) + "\""` — quotes included in the value argument | +| `boolean` / `int` / `long` | The parsed and re-serialized value (`true`, `42`), not the raw input text | +| Empty or unset input | An explicit literal (`""`, a documented default) — a missing value produces `= ;`, another compile error | + +## Edge cases + +| Case | Then | +|------|------| +| The value is a URL with a query string | `"` and `\` are still the only characters that break the literal; `?`, `&`, `=` need no escaping | +| Kotlin DSL `"\"$value\""` | Same rule: the string template concatenates the raw value into Java source | +| The input is a secret injected by CI | Escaping is still required, and the probe runs with a synthetic value containing the hostile characters, never the real secret | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Write `buildConfigField("String", "X", "\"$value\"")` with the raw input | Route the value through the escaping helper | A `"` in the input closes the Java literal early | +| Defer all validation to app start-up | Add a compile probe with hostile characters to the verification step | javac fails first, so the runtime check never sees the bad value | + +## Sources + +- https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/VariantDimension — `buildConfigField`: "The field is generated as: ` = ;` This means each of these must have valid Java content. If the type is a String, then the value should include quotes." +- https://developer.android.com/build/gradle-tips — official example `buildConfigField("String", "BUILD_TIME", "\"${minutesSinceEpoch}\"")`, the value argument carrying its own quotes +- Field evidence 2026-09-27 (linkly-calendar Android, t2-dev-env design review): the generated line `public static final String LINKLY_API_BASE_URL = "http://a"b";` compiled with javac 17 exited 1 with `unclosed string literal`; the same line with the quote escaped exited 0. The plan's "validate at runtime" step could not run on the failing build diff --git a/wiki/infrastructure/index.md b/wiki/infrastructure/index.md index 2c10a08..5248d92 100644 --- a/wiki/infrastructure/index.md +++ b/wiki/infrastructure/index.md @@ -26,7 +26,7 @@ Match your situation to a "load when" line; load only matching pages. | [usage-limit-paused-workers](agent-orchestration/usage-limit-paused-workers.md) | Several workers billed to one account go quiet within minutes of each other while every liveness check passes; a worker's terminal shows a `You've hit your session/weekly/Opus/Sonnet limit · resets …` notice; deciding whether to restart, replace, or wait on a worker with no task-level error; writing the prompt that resumes a worker after a usage window resets; a subagent call (auditor, reviewer) fails at once with `rate_limit`/HTTP 429 naming one model (e.g. a Fable limit) while the session keeps working | | [worktree-isolated-workers](agent-orchestration/worktree-isolated-workers.md) | Authoring the brief/output contract for parallel workers each confined to its own git worktree; workers stall at the same phase with no task-level error; deciding where shared or produced artifacts live and which direction (read vs write) a worktree guardrail stops; a guardrail escalates on read-only access to another worktree; the isolation guard is a Bash-command hook while workers also edit files with native Edit/Write tools; a worktree-escape escalation arrived or the main checkout shows worker-made edits that must be transferred by patch; a worker's tool resolves its `.dev-loop`/state directory against the main checkout from inside a worktree, or a pre-write escape escalation names a state directory; a worker's tool byproduct directory (`.dev-loop/`, `.orchestration/`) dirties `git status` and the fix under consideration is editing the tracked `.gitignore` | | [autonomous-decision-rulings](agent-orchestration/autonomous-decision-rulings.md) | An unattended agent hits a decision its plan does not answer and must choose between stopping to ask and proceeding; a run stalls on questions no human needed to see; deciding which decision categories require a human; recording autonomous decisions for audit; resuming after interruption/compaction without re-dispatching completed work | -| [checkable-claims-in-an-adopted-plan](agent-orchestration/checkable-claims-in-an-adopted-plan.md) | A worker adopts a coordinator-authored plan that states derived numbers (contrast ratios, orderings, hex arithmetic), a fixed symbol contract other tasks consume, or a deliverable file downstream tasks read; deciding what to recompute before encoding the plan's claims in tests; you are the plan's author about to state a two-site wiring (threaded value, id-keyed lookup) or a pre-computed contrast set as fact; a declared deliverable turns out gitignored; a discrepancy with the plan is found and you are deciding whether to correct it locally or escalate; adopting a multi-task plan with a "Task order / Depends on" table before implementing any task in it, to check the table against each task's own Steps prose for the real dependency direction; writing such a plan's own "reuse existing X" / "Y supports Z" claims | +| [checkable-claims-in-an-adopted-plan](agent-orchestration/checkable-claims-in-an-adopted-plan.md) | A worker adopts a coordinator-authored plan that states derived numbers (contrast ratios, orderings, hex arithmetic), a fixed symbol contract other tasks consume, or a deliverable file downstream tasks read; deciding what to recompute before encoding the plan's claims in tests; you are the plan's author about to state a two-site wiring (threaded value, id-keyed lookup) or a pre-computed contrast set as fact; a declared deliverable turns out gitignored; a discrepancy with the plan is found and you are deciding whether to correct it locally or escalate; adopting a multi-task plan with a "Task order / Depends on" table before implementing any task in it, to check the table against each task's own Steps prose for the real dependency direction; writing such a plan's own "reuse existing X" / "Y supports Z" claims; an adopt-only plan states both "do not modify existing test bodies" and "stop on any new failure" and you must learn whether its rule flips an existing assertion | | [worker-reported-plan-contradiction](agent-orchestration/worker-reported-plan-contradiction.md) | A coordinator's design doc and per-task step files disagree on a concrete, checkable fact and a worker reports it; deciding whether to re-assert the design doc or verify empirically; a widened/narrowed catch clause, condition, or table is the disputed change; the worker may have misread a document instead of the documents actually disagreeing | | [session-context-token-budget](agent-orchestration/session-context-token-budget.md) | Planning or running long-lived coordinator/worker agent sessions and deciding when to compact or clear context; a run's cost is dominated by cache reads; screenshots or large file reads are entering a long-lived session; choosing slot counts / per-phase token budgets for an orchestrated run | | [code-graph-as-orientation-layer](agent-orchestration/code-graph-as-orientation-layer.md) | A repository carries a locally built code knowledge graph (graphify `graphify-out/graph.json` or similar) and an agent is about to plan, decompose, or estimate the blast radius of a change; an orchestrator needs each parallel task's file set before dispatch; deciding whether a graph hit can stand as plan evidence; checking whether the graph is fresh enough to use; the tool's git hooks are installed and the graph must survive a `git pull` or a fresh clone; deciding who may build or refresh the graph (an agent versus a user-installed git hook) | @@ -58,6 +58,7 @@ Match your situation to a "load when" line; load only matching pages. |------|-----------| | [environment-config](config/environment-config.md) | Adding configuration that differs per environment; a bug traced to a dev/stg/prd config difference; config sprawled across hardcoded values, files, and env vars; reviewing how a service gets its settings | | [path-valued-config](config/path-valued-config.md) | A config key or env var holds a filesystem path (spool/input/output directory, data file, socket) for a process whose working directory is set by launchd/systemd/cron/a container entrypoint/CI; deciding whether to accept a relative path, expand `~`, or crash at startup; a correctly-deployed service processes nothing and reports no error; writing the loader's rejection tests | +| [config-values-emitted-as-source-text](config/config-values-emitted-as-source-text.md) | A build step writes a user- or environment-supplied config value into generated source a compiler parses (AGP `buildConfigField` into `BuildConfig.java`, a codegen constants file) and the design defers validation to runtime; deciding how to escape the value and where a hostile-character probe belongs in the verification step | | [keys-ahead-of-their-consumer](config/keys-ahead-of-their-consumer.md) | Adding a config key that a component outside your repository parses, for a version of it that has not shipped yet; reviewing a change justified by "older versions ignore unknown keys"; deciding whether a parser drops or rejects an unknown key (path query vs permissive binding vs strict schema); pre-declaring a key that carries a security control | ## containers diff --git a/wiki/testing/index.md b/wiki/testing/index.md index 549a4a5..411ff15 100644 --- a/wiki/testing/index.md +++ b/wiki/testing/index.md @@ -46,6 +46,7 @@ Match your situation to a "load when" line; load only matching pages. | [assertion-scanner-false-positive-on-unittest-convention](quality/assertion-scanner-false-positive-on-unittest-convention.md) | A regex-based test-quality floor that greps for bare `assert`/`pytest.raises` reports "no assertion" or "no error case" on files written in unittest's `self.assertX` convention, or reports `no-tests` on a file whose tests live inline in source under a convention its path/name classifier has no pattern for (Rust `#[cfg(test)] mod tests` in `src/*.rs`); a worker reports such a floor failure and you must decide whether to trust it, re-run the canonical checker, or change the tests | | [store-assertions-after-a-rolled-back-run](quality/store-assertions-after-a-rolled-back-run.md) | Writing a test that asserts on repository/store state after a workflow, job, or interpreter run that failed partway through when the runner rolls the store back on any non-completed status; a mid-run write "visibly landed" but the post-run assertion finds a missing key or stale value; choosing between store reads and trace/log entries as the assertion target | | [gate-parsing-vs-command-execution](quality/gate-parsing-vs-command-execution.md) | Writing or reviewing a gate script that reads a document/plan containing command strings and deciding whether the parser may run them directly (`sh -c`) or must hand them to a dedicated timeout-bounded executor; proving a parser does not execute embedded commands | +| [alias-table-contract-tests](quality/alias-table-contract-tests.md) | Writing or reviewing the test for a compatibility alias layer (deprecated token/typography/colour/enum/function names forwarding to new canonical ones) that a later task migrates call sites against; a review finds the test asserting a hand-picked subset of the rows; deciding where the expected column comes from and how to prove the table test can fail | | [spec-artifact-checks](quality/spec-artifact-checks.md) | Authoring or reviewing the check itself: that a mapping table covers every rule/field/enum case, that ids resolve across documents; deciding whether a green check earned "verified" or only "present"; designing one negative control per check in a multi-check harness; parsing Markdown table rows programmatically in a doc-as-spec repo (deciding whether a passing gate is enough to *accept the deliverable* → wiki/qa/document-verification/spec-document-gates.md) | | [schema-additions-under-a-golden-gate](quality/schema-additions-under-a-golden-gate.md) | Adding a node kind, variant, discriminator value, or field to a document format (IR, JSON Schema, spec artifact) whose only automated gate builds its negatives by mutating one committed golden example; the gate or the whole suite comes back green right after a schema change; deciding which negative each new schema keyword needs, and whether a green suite that never loads the schema is evidence at all | | [policy-at-several-return-sites](quality/policy-at-several-return-sites.md) | One function applies the same policy at more than one of its own success returns (a CLI handler computing an exit code under `--strict`/`--check` and returning it from several branches, a controller stamping one header on several 200s); judging whether a suite covers such a flag when only one green test exists; choosing test cases by exit path rather than by input boundary; proving each site with a reversion of that site alone | @@ -88,6 +89,7 @@ Match your situation to a "load when" line; load only matching pages. | [captured-call-arguments](mocking/captured-call-arguments.md) | Writing the spy/stub test that holds a fix to one argument of one wiring call (constructor, factory, server startup); such a test is green while a mutation of a *different* argument of the same call survives; the fix extracted the value into a resolver and you are choosing what to assert; deciding between asserting a constant's value and asserting that the call site passes it on; choosing how to record an argument you deliberately leave unpinned; a CLI flag is also declared on a long-lived server subcommand but only a boot-time probe reads it and nothing threads its value to the consuming constructor | | [extracted-method-this-binding](mocking/extracted-method-this-binding.md) | Code extracts a method off a class instance into a variable (`const fn = obj.method`) or passes `obj.method` as a callback, and the unit tests inject it as `vi.fn()`/`jest.fn()`; a feature throws `TypeError` in the browser or an E2E run while its unit tests are green; choosing a regression test a `this`-indifferent mock cannot satisfy (a `this`-reading callable, `mock.contexts`) | | [producer-wire-format-in-mocks](mocking/producer-wire-format-in-mocks.md) | Consumer logic keys on an identifier another process produces (correlation/request/run id) and is tested only against a mock that invents that id; a covered/matched matrix is full in tests and empty in the real app; deciding what a mock may assume about an id's shape and whether the consumer may parse it | +| [fake-intersection-observer-for-viewport-animations](mocking/fake-intersection-observer-for-viewport-animations.md) | Unit-testing (jsdom/vitest/jest) an entrance animation that starts on viewport entry through a library wrapping `IntersectionObserver` (motion/framer-motion `whileInView`) with the observer replaced by a fake class; a constructor-count probe reads 0 from the second test in a file; timing tests (stagger/delay) pass in ~1 ms; deciding what the fake entry must carry (`target`) and what to count (`observe(element)`) | | [autouse-fixture-shadows-function-under-test](mocking/autouse-fixture-shadows-function-under-test.md) | A test calls `module.func(...)` directly to assert that function's own properties, while a conftest `autouse=True` fixture `monkeypatch.setattr`-replaces the same name; deciding whether an assertion target is the real function or the fixture's stub; capturing the original before the fixture applies | ## flaky diff --git a/wiki/testing/mocking/fake-intersection-observer-for-viewport-animations.md b/wiki/testing/mocking/fake-intersection-observer-for-viewport-animations.md new file mode 100644 index 0000000..9cbb963 --- /dev/null +++ b/wiki/testing/mocking/fake-intersection-observer-for-viewport-animations.md @@ -0,0 +1,83 @@ +--- +id: testing-mocking-fake-intersection-observer-for-viewport-animations +domain: testing +category: mocking +applies_to: [general, react] +confidence: verified +sources: + - framer-motion 13.2.0 `dist/es/motion/features/viewport/observers.mjs` (read 2026-09-28) — `observers` WeakMap keyed by root then `JSON.stringify(options)`; `fireObserverCallback` reads `observerCallbacks.get(entry.target)`; `observeIntersection` calls `observer.observe(element)` per element + - https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/target + - https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/observe + - https://vitest.dev/api/vi.html +last_verified: 2026-09-28 +related: [testing-quality-tests-that-cannot-fail, testing-mocking-what-to-mock, testing-async-async-testing, frontend-data-fetching-infinite-scroll, testing-quality-alias-table-contract-tests] +--- + +# A Fake IntersectionObserver Under a Viewport-Triggered Animation Library + +## When this applies + +Unit-testing in jsdom (vitest/jest) a component whose entrance animation starts +on viewport entry through a library that wraps `IntersectionObserver` — motion / +framer-motion `whileInView` + `viewport`, or any wrapper that keeps its own +observer registry — with `IntersectionObserver` replaced by a fake class, and +you want to assert "this element registered a viewport trigger" or "the +entrance animation ran with these timing props (stagger, delay, duration)". + +## Do this + +1. **Count `observe(element)` calls, not constructor calls, and assert the + observed node's identity (`toBe(node)`).** The wrapper caches one observer + per `(root, serialised options)` pair in a module-level `WeakMap`, so the + constructor runs once per module lifetime: from the second test in a file it + is called 0 times, and a constructor probe fails on a correct implementation. + `observe` is called once per registered element, so an element count is + immune to the cache and to test order. +2. **Record `observe()` calls in a file-level list that `beforeEach` clears, + not in per-instance fields.** The instance the wrapper cached in the first + test is the one every later `observe()` reaches — a fresh fake class stubbed + per test is never constructed again. +3. **Give the fake entry a `target`:** `this.cb([{ isIntersecting: true, + target }], this)`. The wrapper resolves the per-element callback by + `entry.target`; an entry without it makes the lookup return `undefined`, the + callback becomes a silent no-op, and the animation never starts — while the + first-paint styles (`opacity: 0`, `translateY(16px)`) still match, so the + suite stays green. +4. **Pair every timing assertion with a positive control** that waits for the + element to reach its visible end state (`opacity: 1`) — a suite whose + animation never fired is green for every mutation of the transition props. +5. **Mutation-check the prop plumbing once:** hardcode the defaults in place of + `staggerChildren` / `delayChildren` / `duration` and require red + ([testing-quality-tests-that-cannot-fail]). Record the red output in the + task report. + +| You want to prove | Assert | +|-------------------|--------| +| The element registered a viewport trigger | `observe` was called with exactly that node (count and identity) | +| The entrance animation ran | The awaited end-state style, after firing the entry with `target` | +| Stagger/delay reach the transition | A timing difference between siblings after entry, plus the mutation from step 5 turning red | + +## Edge cases + +| Case | Then | +|------|------| +| The constructor count is 0 in every test after the first | That is the cache, not a defect; move to `observe` counting (step 1) | +| Items with their own `whileInView` are a bug you want to catch | The element count exposes it (`expected [Array(4)] to have a length of 1 but got 4` on a mutant that gave each item its own trigger) | +| Timing tests run in ~1 ms and pass | The animation never fired; check the fake entry for `target` — a real run takes about the animation's duration (1 ms → 1018 ms after the fix) | +| Two viewport option sets are in play (`once`, `margin`, `amount` differ) | Each set gets its own cached observer; count `observe` per element, not per observer | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Count `new IntersectionObserver(...)` calls to prove registration | Count `observe(element)` calls and assert the node | The observer is cached per root + options in a module WeakMap | +| Fire `[{ isIntersecting: true }]` without `target` | Include `target` on every fake entry | The wrapper looks the callback up by `entry.target` | +| Accept a green timing suite as proof that props reach the transition | Add an end-state wait and a hardcoded-defaults mutation | Without a fired entry, only first-paint styles are observable | + +## Sources + +- framer-motion 13.2.0 `dist/es/motion/features/viewport/observers.mjs` (read 2026-09-28) — `initIntersectionObserver` stores observers in `observers: WeakMap`; `fireObserverCallback = (entry) => { const callback = observerCallbacks.get(entry.target); callback && callback(entry); }`; `observeIntersection` sets the callback for the element and calls `observer.observe(element)` +- https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/target — `target` is the element whose intersection changed; the wrapper keys its callbacks on it +- https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/observe — one `observe()` call per element added to the observed set +- https://vitest.dev/api/vi.html — `vi.stubGlobal` replaces a global for the test; it does not reset module-level state inside an already-imported library +- Field evidence 2026-09-27 (cover-letter, t1-motion-stagger, vitest + jsdom): constructor probe on a single group mount read `expected +0 to be 1`; the element probe read 1 on the correct implementation and `expected [ Array(4) ] to have a length of 1 but got 4` on a mutant giving each item its own `whileInView`. Without `target` on the fake entry, all 10 tests were green and so was a mutant hardcoding `transition: { staggerChildren: 0.06, delayChildren: 0 }`; with `target`, the same mutant turned 2 timing tests red (`expected '0.250279879765003' to be '0'`) and the run time went from 1 ms to 1018 ms diff --git a/wiki/testing/quality/alias-table-contract-tests.md b/wiki/testing/quality/alias-table-contract-tests.md new file mode 100644 index 0000000..4b23e09 --- /dev/null +++ b/wiki/testing/quality/alias-table-contract-tests.md @@ -0,0 +1,69 @@ +--- +id: testing-quality-alias-table-contract-tests +domain: testing +category: quality +applies_to: [general] +confidence: field-tested +sources: + - https://pitest.org/quickstart/basic_concepts/ + - https://testing.googleblog.com/2021/04/mutation-testing.html + - https://junit.org/junit5/docs/current/user-guide/ +last_verified: 2026-09-28 +related: [testing-quality-spec-artifact-checks, testing-quality-tests-that-cannot-fail, testing-quality-minimum-case-set, infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan, testing-mocking-fake-intersection-observer-for-viewport-animations] +--- + +# Testing a Compatibility Alias Table Row for Row + +## When this applies + +A task ships a compatibility layer of deprecated names forwarding to new +canonical ones — design-token roles, typography styles, colour names, enum +members, function or type aliases — that a later task will migrate call sites +against, and you are writing or reviewing the test that pins the mapping. Also +when a review finds such a test asserting a hand-picked subset of the rows. + +## Do this + +1. **Assert every row from a table whose expected column is transcribed from + the design document's mapping**, not read back from the alias declarations. + A test that derives its expectations from the implementation passes on any + mapping, right or wrong. +2. **Assert the table's size equals the legacy-name count** the same document + states, and — where the language enumerates the legacy set (an `enum`'s + `values()`, a sealed hierarchy, an exported list) — assert that every member + has a row. An alias missing from the test is then a failure, not a gap. +3. **Prove detection once:** retarget one alias (or flip one expected cell) and + require exactly that row to fail; record the red output in the task report + ([testing-quality-tests-that-cannot-fail]). +4. **Keep the table in the consumer-facing shape** (legacy name → canonical + name) so the migration task can diff it against its own call-site rewrite. + +| Situation | Do | +|-----------|----| +| The mapping is in a design doc's decision table | Copy each row into the test as data; iterate with a parameterized test (`@ParameterizedTest`, `test.each`, a `forEach` over a list of pairs) | +| The legacy names are an enum | Iterate `values()` and look each up in the table; assert `values().size == table.size` | +| A legacy name is intentionally removed rather than aliased | Give it an explicit row with a removal sentinel instead of omitting it, so absence stays a failure | +| The document and the code disagree on a target | Escalate per [infrastructure-agent-orchestration-checkable-claims-in-an-adopted-plan]; do not copy the code's value into the test | + +## Edge cases + +| Case | Then | +|------|------| +| The alias forwards through a property or function, not a constant | Assert on the resolved value (the object the alias returns), not on the alias's declaration text | +| Two legacy names map to one canonical name | Two rows, same target — the size assertion counts legacy names, not targets | +| The document's table has more rows than the code declares | The size assertion fails first; that is the finding — report it before adding rows to the code | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Spot-check four of eighteen aliases | Assert all rows plus the count | Any un-asserted alias can forward to the wrong target and only a screen migration notices, visually | +| Build the expected column by reading the alias definitions | Transcribe it from the design document | Expectations derived from the implementation cannot disagree with it | +| Stop at "the test passes" | Retarget one alias and watch that row fail | A table test that cannot fail proves nothing about the mapping | + +## Sources + +- https://pitest.org/quickstart/basic_concepts/ — a mutant is killed when a test fails on the mutated code; a surviving mutant marks a test that does not detect the change +- https://testing.googleblog.com/2021/04/mutation-testing.html — mutation testing as the check that tests actually detect behavioural changes, beyond coverage +- https://junit.org/junit5/docs/current/user-guide/ — `@ParameterizedTest` with `@MethodSource` / `@CsvSource` runs one invocation per row of a data table +- Field evidence 2026-09-27 (linkly-calendar Android, t1-design-tokens): review round 1 found the typography alias test asserting 4 of 18 legacy names; the replacement test iterated all 20 `Typography.kt` enum entries against the plan's D4 mapping table with a size assertion, and the round-2 reviewer retargeted `ButtonMd` and saw exactly that test fail From 07080829dbc44839545bab74126f517813fe9100 Mon Sep 17 00:00:00 2001 From: choiyounggi <74581798+choiyounggi@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:56:31 +0900 Subject: [PATCH 2/2] knowledge: fold a second field hit (suspend member inside runTest) into implicit-receiver-shadowing-in-scope-functions --- .../kotlin/implicit-receiver-shadowing-in-scope-functions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wiki/backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md b/wiki/backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md index 0c4bf06..aaddbb5 100644 --- a/wiki/backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md +++ b/wiki/backend/java/kotlin/implicit-receiver-shadowing-in-scope-functions.md @@ -48,6 +48,7 @@ the fake's own exception on a line that "calls the helper". | Case | Then | |------|------| | The receiver member is `suspend` and the caller is not in a coroutine | The compiler reports the suspend-call error, which is the first visible symptom — read it as shadowing, not as a missing `runBlocking` | +| The receiver member is `suspend` and the call sits inside `runTest`/`runBlocking` or another coroutine | It compiles cleanly and fails only at runtime with the fake's own stub error (`NotImplementedError`, `NoSuchElementException`) on the line that "calls the helper" — resolve the call before touching the fixture data | | Nested receivers (`a.apply { b.apply { trip() } }`) | The innermost receiver wins, then outer receivers, then top-level — the spec orders implicit receivers by priority before top-level callables | | An extension function shares a member's name | The member wins on an explicit receiver too; rename the extension | @@ -63,3 +64,4 @@ the fake's own exception on a line that "calls the helper". - https://kotlinlang.org/spec/overload-resolution.html — "Call without an explicit receiver": for an identifier `f` the sets are analyzed in order: local callables, then "the overload candidate sets for each pair of implicit receivers … in order of the receiver priority", then "top-level non-extension functions named `f`"; the first non-empty set wins - https://kotlinlang.org/docs/scope-functions.html — `apply`/`run`/`with` expose the context object as `this`; `also`/`let` expose it as `it` - Field evidence 2026-09-27 (linkly-calendar Android, `TripViewModelTest`): three cases failed with the fake's `NoSuchElementException("no trip t1")` thrown from inside `apply {}`; the top-level `trip(id)` fixture had been shadowed by `FakeTripApi.trip(id)`. Renaming the helper to `tripFixture` made all 13 pass +- Field evidence 2026-09-28 (the same Android app, `CalendarViewModelTest`, inside `runTest`): three cases failed with `kotlin.NotImplementedError: not used by CalendarViewModelTest` thrown by the fake's `suspend fun trip(...)` from inside `FakeX().apply { … }`; the top-level fixture `trip()` renamed to `makeTrip()` — 14/14 pass. Second independent hit of the same resolution rule; the `suspend` member compiled without complaint because the call site was already in a coroutine