Development Workflow
1. Scope
This document defines the current ViewCompose collaboration workflow. It addresses two recurring problems: multi-phase work accumulates unrelated changes, and interrupted tasks need a reliable way to recover context. Follow this workflow unless a task explicitly requires a different process.
2. Small commits
Commit each independently verifiable step as soon as it is complete. A small step has one describable goal, one validation path, and does not depend on bundling unrelated work.
Examples include a focused plan, a minimal host abstraction, one independent bug fix, one test group, one Demo page, or one instrumentation regression.
Do not combine unrelated bug fixes or leave planning, broad implementation, and multiple test groups uncommitted in the worktree for an extended period.
2.1 Foundational-instability preemption
An unstable foundational contract, ownership model, or core implementation takes the highest priority over release-window convenience, the current plan phase, and additive roadmap work. Once evidence confirms that the design itself is unsound, stop dependent expansion, replace it at the owning layer, migrate callers, and remove the old API, transport, fallback, and compatibility branches in one hard cut. Do not defer a confirmed foundational correction to a later phase. A timing guard, deprecated no-op field, caller-specific wrapper, or parallel legacy path is not an acceptable substitute for correcting the model. Record the evidence, migration, and release impact explicitly.
3. Documentation synchronization
Before implementation, classify documentation impact with the
change-impact matrix. A PR lists every KDoc,
Javadoc, module manual, or cross-module document updated. A No documentation impact result states
why public API, behavior, architecture, compatibility, and maintenance workflow are unchanged.
Before adding or changing a public/protected API, assign its Q level and satisfy every applicable
parameter, return, state, lifecycle, threading, failure, and platform contract in the same PR under
the Source Documentation and API Comment Standard. A Q3 API also
ships a compiled @sample. Existing documentation debt cannot justify new debt.
The same change supplies a structured capability-impact record conforming to the Governance V2
public capability impact contract. Resolve
the stable capability owner and every KDoc, module, sample, Reference, Tutorial, Guide,
Architecture, Migration, and redirect disposition before implementation. Add one immutable record
under docs/project/records/documentation-governance-v2/impacts/ for each detected structural
capability change. The compiled gate compares it with the exact pull-request base and rejects
missing, reused, stale, duplicated, or mismatched impact ownership.
Update documentation before or together with:
- a new capability direction;
- an architecture boundary change;
- a test strategy;
- a Demo module plan;
- a host or container semantic change;
- resolution of recorded debt, architecture work, or roadmap items.
Start with the owning current document, such as the architecture overview, roadmap, theme architecture, Modifier architecture, or NodeSpec specification.
When code resolves a documented problem, update that document in the same or immediately adjacent commit. Current-problem, remaining-work, and next-step sections cannot lag the implementation.
3.1 Release intent for independently published modules
Every pull request must classify Maven release impact before merge. When automatic ownership finds
publication-relevant source, module build metadata, or compiled API sample changes, add one new
immutable release/changes/<unique>.json file for the pull request. Use breaking, feature, or
fix for a direct artifact change. Use ignored only with a concrete reason when the detected
path does not change the published contract or artifact. Never write dependency; the release
planner derives reverse-dependency propagation from the current Gradle project graph.
Test-only, Demo, benchmark, and handwritten documentation changes are release-neutral by default. Shared root build inputs must either declare the affected artifacts or record a concrete no-release classification because path ownership alone is insufficient. Changesets are append-only after merge and remain in the repository as the audit trail. Squash and rebase workflows do not alter this contract: release intent belongs to the pull request, not each intermediate commit.
Run ./gradlew verifyViewComposeReleaseIntent locally. It is part of qaQuick; CI compares the PR
to its exact base SHA. Release owners use planViewComposeRelease and
prepareViewComposeRelease as defined in Publishing.
4. Tests and Demo assets
An implemented capability normally includes, in order, unit tests, a Demo scenario, and required Demo UI tests. If one cannot be delivered, state what is missing and why in the commit or PR.
4.1 Completion commands
- Fast gate:
./gradlew qaQuick - Preview snapshot gate:
./gradlew qaPreview - Full gate:
./gradlew qaFull
qaQuick compiles core modules, runs unit tests, and enforces the canonical documentation gate,
including language placement and reviewed translation fingerprints. qaPreview runs
:viewcompose-preview:verifyPaparazziDebug as an independent visual CI check. A visual change may
update committed baselines only after the generated images and differences have been reviewed;
never record an unexplained mismatch merely to make the gate pass.
Run repository Gradle gates on JDK 21. The current build baseline is Gradle 9.3.1, Android Gradle
Plugin 9.1.1, and Kotlin 2.2.10. Published runtime libraries still target Java 11 bytecode. The
repository temporarily retains the explicit Kotlin Android plugin and legacy Android DSL through
android.builtInKotlin=false and android.newDsl=false; remove both only in one coordinated
convention-plugin/publication migration because AGP 10 removes that opt-out. Preview compiles
against SDK 37. One repository-owned test resource deliberately pins the default Android
Robolectric runtime to supported SDK 35; a test may select an older SDK only when it names and
proves an API-level-specific compatibility contract. Neither test choice lowers a production
compile SDK.
Pull-request workflows first run the standalone planPullRequestImpact entry point from
tools/viewcompose-quality-build; this configures only that included build, not the Android
multi-project build. The classifier reads the exact base-to-head Git diff, the publishing artifact
catalog, the dependency contract, and the frozen full-fallback policy. Its JSON and job summary list
selected gate families, direct artifacts and non-published projects, transitive dependencies,
reverse dependents, reasons, workflow selection, and full-fallback status. Documentation and
website-only changes can skip Android child work. Module production changes select release,
API-documentation, documentation, module, and any graph-reachable Preview gates; module test, Demo,
sample, integration, and benchmark paths retain their explicit families and project ownership.
More than 300 changed paths, an unknown path, a sensitive shared input, a non-pull-request event,
an empty diff, or the full-verification pull-request label selects every current workflow.
An added release/changes/*.json file is classified as append-only release intent and can accompany
an otherwise scoped production change. Modifying, deleting, copying, or renaming a Changeset still
selects complete verification, as do release registries and release tooling. This is a hard
separation between immutable intent additions and mutable release infrastructure; it is not a
general relaxation of release/**.
The branch-protection contexts remain exactly qaQuick and Build documentation; qaPreview is
visible but is not currently a required context. Each visible context is an always() result facade:
it succeeds for intentionally skipped child work only when classification succeeded and selected
false, and it fails when planning fails or selected work does not succeed. Required workflows still
trigger for every pull request, so path filtering cannot leave a required context pending. Every
main or manual run selects complete verification; documentation deployment remains possible only
after the complete documentation child and its facade both succeed.
The selected documentation child plans a generator fingerprint and a complete immutable-history
fingerprint before restoring generated API candidates. Pull requests never save this cache; a
successful main child is its only writer. A restored key is only a hint: per-source-revision
entry sets and every file size/SHA-256 digest must verify before reuse, while a stale or corrupt
group is deleted and regenerated. The job summary reports hit, partial, miss, recovery, reused and
generated group counts, invalid groups, bounded parallelism, and assembly duration. Source,
language, and translation checks run once through verifyDocumentationStructure; CI generates the
site catalog once, then calls the prepared type-check and build entry points to avoid repeating npm
prebuild hooks.
The classifier owns a typed qaQuick execution mode in addition to the stable qa_quick workflow
selection. skip omits Android work, complete runs only full qaQuick, affected-with-shadow
runs qaAffected followed by full qaQuick, and affected runs only qaAffected. The workflow
validates the selected mode and both step outcomes before its required qaQuick facade can pass;
an unsupported mode or an unexpected executed/skipped outcome fails closed.
A scoped candidate uses a 4 GiB Gradle heap and at most two workers. The root build independently
reconstructs the current
api/implementation/compileOnly/runtimeOnly project graph, rejects any classifier closure
drift, and selects compile plus unit-test tasks from the configured projects rather than an artifact
task list. Demo, sample, integration-test, and benchmark ownership selects its project-specific
tasks; local Maven publication is included only for sample consumers. Documentation and Preview
remain owned by their independently visible workflows.
The no-shadow affected mode is deliberately narrow. One accepted class requires exactly
documentation-governance, documentation-site, and Tutorial-sample gate ownership,
:samples:tutorials as the sole non-published project, no published artifact, and no full-fallback
reason. The second requires the exact documentation-governance, documentation-site,
module-verification, Preview, release-intent, and sample families plus at least one published-module
src/test/samples source. Its remaining paths are limited to documentation, current Chinese
mirrors, the generated capability catalog, append-only Changesets, Tutorial main sources, and an
optional Counter debug Preview source. Module production, build scripts, ordinary tests, and
deleted or renamed code stay affected-with-shadow, where both candidate and complete gates must
succeed. main, manual runs, and every full-fallback pull request use complete. Demo,
integration, benchmark, shared-input, unknown, and every unaccepted module class also retain the
complete comparison. This does not change the project-wide local Gradle default.
The 2026-08-26 local acceptance used one real historical Paging diff. qaAffected selected 39 task
paths across one direct and ten dependency artifacts and passed in 2 min 6 s (215 actionable;
188 executed and 27 up-to-date). Complete qaQuick then passed in 8 min 5 s (2,342 actionable;
2,096 executed and 246 up-to-date), so candidate duration was 74.0% lower and the local
execution-work conclusion is improved. Both outcomes matched. This is not rollout latency
proof: it is one developer-machine sample, both paths inherited local caches, and the candidate ran
first and partially warmed the complete gate. Hosted latency therefore remains inconclusive;
keep the complete shadow until the Phase 6 change-class corpus satisfies the plan's observation
and correctness criteria.
Pull request #173 supplied the first hosted full-fallback acceptance for this implementation. The
candidate correctly skipped, complete qaQuick passed in 19 min 37 s, qaPreview in 8 min 41 s, and documentation work in 5 min 12 s; all facades passed. Relative to the immediately
preceding accepted pull request, qaQuick changed by -0.17% and qaPreview by -0.19%, both
no material change. Documentation changed by -14.8%, but different immutable-cache state and
inputs make that latency result inconclusive. One full-fallback sample proves the behavior but
not a distribution; the next action remains collection of scoped change-class observations.
Eleven comparable hosted documentation/Tutorial-sample pull requests (#177, #178, #179, #180,
#182, #183, #184, #185, #203, #204, and #205) supplied the accepted no-shadow corpus. Each selected
1,176 actionable candidate tasks, selected no published artifact, and reached the same successful
result as the following 2,342-task complete shadow. Reconstructing the required critical path as
the maximum of candidate completion from job start and the parallel documentation-child duration
gives nearest-rank P50 6 min 22 s and P95 7 min 17 s; both satisfy the 8 min/12 min
thresholds. All 11 documentation children restored and verified 5/5 immutable API groups with
zero generation or invalid group, a 100% exact-hit rate. The scope and cache conclusions are
improved, and correctness is no material change with zero divergence. At acceptance time,
the observed post-cut timing was still inconclusive because it was reconstructed from shadow
runs; an actual critical path was still required. This evidence enabled the first exact affected
class.
Pull request #225 supplied that class's first real post-cut no-shadow run. Its five-task candidate
covered only :samples:tutorials, passed in 6 min 9 s, and reached the required facade in
8 min 12 s from workflow creation; complete work was skipped. This is 72.9% below the Phase 0
execution P50 and 66.0% below its end-to-end P50. Scope and latency are improved and
correctness is no material change. One run does not establish a distribution; nine more
naturally eligible successful runs are required before evaluating this class's P50/P95.
Eleven hosted module-documentation/compiled-sample pull requests (#186, #187, #188, #189, #190,
#191, #194, #195, #198, #199, and #200) supplied the second accepted no-shadow corpus. Each changed
only documentation/governance records, compiled module src/test/samples, bounded Tutorial or
Counter sample sources, an append-only Changeset, Chinese mirrors, and the generated capability
catalog. All affected candidates and following complete shadows succeeded with zero divergence.
The reconstructed no-shadow execution path has nearest-rank P50 8 min 5 s and P95 10 min 4 s;
end-to-end P50 is 9 min 13 s and P95 is 11 min 18 s. Execution P50 is 64.4% below the Phase 0
22 min 43 s comparator. All eleven documentation children reused 5/5 immutable API groups with
zero generation or invalid group. A separate eleven-run successful main corpus has complete
qaQuick job P95 20 min 41 s, 16.2% below Phase 0, so the full path did not regress. Scope,
cache reuse, and latency are improved; correctness is no material change. Pull request #196
remains affected-with-shadow because it changed a module build script, and production source,
ordinary tests, code deletion/rename, sensitive tooling, and unrecognized paths remain outside this
hard cut. At rollout, the first eligible hosted run was still required to record actual post-cut
timing.
Pull request #226 supplied that class's first real post-cut no-shadow run. Its 68 selected tasks
covered 23 published artifacts and :samples:tutorials, passed in 7 min 48 s, and reached the
required facade in 9 min 42 s from workflow creation; complete work was skipped. The execution
and end-to-end paths are 65.7% and 59.8% below Phase 0. Scope and latency are improved and
correctness is no material change because the selected closure, Preview, documentation, and
facades all passed. The merged main revision then passed complete qaQuick in 15 min 23 s with
the work job at 16 min 38 s, 19.6% below the accepted full-main P95; the full path therefore
shows no material change in safety. One run does not establish a distribution; nine more
naturally eligible successful runs are required before evaluating this class's P50/P95.
The first post-cut control window (#219--#223) produced no eligible no-shadow affected run. #219
changed only the active plan and correctly selected skip; the qaQuick facade completed 70 s
after workflow creation and the documentation facade completed in 6 min 29 s, 24 s (-5.8%)
below the comparable #216 skip observation, a no material change latency result with matching
success. #220--#223 correctly selected complete for shared quality/site tooling, publishing build
logic, and release or documentation-history metadata. Their complete Gradle qaQuick steps were
13 min 2 s, 16 min 53 s, 16 min 11 s, and 15 min 2 s; nearest-rank P50/P95 are
15 min 2 s/16 min 53 s, 33.8%/31.6% below the Phase 0 execution comparator. Required
end-to-end critical paths were 15 min 15 s, 24 min 33 s, 18 min 3 s, and 16 min 55 s, so
P50/P95 are 16 min 55 s/24 min 33 s, 29.9%/45.2% below Phase 0. Every gate and required
facade passed. Safety remains no material change and the small heterogeneous complete-path
sample is improved relative to Phase 0; actual no-shadow latency remains inconclusive because
the window contains no target-class run. These five same-day, release-heavy changes are control
evidence rather than a representative distribution. Wait for a naturally eligible documentation/
Tutorial-sample or module-documentation/compiled-sample pull request to extend each post-cut corpus;
do not alter samples only to produce an observation.
gradle/actions/setup-gradle is the sole owner of Gradle User Home caching in every workflow that
invokes Gradle. actions/setup-java installs the required JDK but does not separately cache
Gradle. Every setup-gradle use explicitly sets cache-read-only for any ref other than the
repository default branch: pull requests and non-default branches can restore entries, while only
default-branch jobs may write them. Do not add a parallel actions/cache Gradle-home entry or
restore setup-java cache: gradle; optional Gradle build-cache and configuration-cache adoption
requires separate measured acceptance.
The first isolated Paging candidate probe found Build Cache promising but not ready for required
CI. With dependencies prewarmed and outputs cleaned, the no-cache baseline passed in 80.88 s;
clean cache restores passed in 6.04 s and 5.45 s (-92.5% and -93.3%) with 108 of 215
actionable tasks restored from a 9.9 MiB cache. The result was correct on every run, so the local
conclusion is improved. Hosted portability, main-to-PR reuse, aggregate size, and eviction are
unmeasured, so Build Cache remains disabled. Configuration Cache also remains disabled: the same
candidate stored and reused locally in 4.78 s and 1.29 s (-73.0%), but current CI has no
second identical invocation and no configured encrypted cross-job transport. Its current-path
conclusion is no material change. Revisit either option only through a separate hosted shadow;
do not combine adoption with unrelated gate or documentation work.
qaFull adds the application, Counter sample, and tutorial connected tests to qaQuick. Every
repository connectedDebugAndroidTest entry first runs verifyConnectedAndroidDeviceReady. The
preflight requires exactly one online device unless ANDROID_SERIAL selects one, completed boot,
an awake display, and no showing keyguard. It deliberately does not bypass a secure lock screen:
wake and unlock the selected device before retrying. Before marking a capability complete,
qaFull normally passes; a missing device or temporary exemption is recorded in the roadmap with
scope and deadline.
4.2 Remote Demo APK
Maintainers can run the Demo APK workflow from the GitHub Actions page when an installable build
is needed away from a development machine. The workflow builds the debug-signed app APK from the
selected Git ref and uploads it with a SHA-256 checksum and build metadata for 14 days.
The artifact is intended for manual framework verification and is not a release package.
5. Code ownership and placement
Choose the owning module and directory before creating a file. Do not flatten new code into the nearest directory or mix platform, DSL, runtime, and Demo responsibilities.
Decide in this order:
- layer and module ownership: Kernel, UI Foundation, Android Engine, Design System, Integrations, aggregate, tooling, or app;
- directory ownership, for example
context/,dsl/,runtime/,view/, ordefaults/; - file name.
Read the relevant architecture and neighboring module code before implementation. If working code has an obviously wrong home, correct the structure in the current change. Module and directory ownership are required review items.
5.1 Anti-flattening
- A source directory should contain at most 12 files; split by responsibility above that size.
- Split by domain or component family, not author or temporary phase.
- Directory movement updates the architecture directory baseline in the same change.
- Directory movement does not change public API by default. A required package/API change is a separate commit with migration guidance.
5.2 Environment sources
- Host environment semantics come from
viewcompose-ui-foundation/context/EnvironmentandUiEnvironment. - Android extraction enters
UiEnvironmentValuesthroughAndroidEnvironmentBridge. - Renderer does not create another semantic channel; it uses only internal platform conversion in
viewcompose-renderer-android/view/DimensionUtils.kt. - Renderer containers do not add private density caches or dp/sp conversion helpers.
- Correct existing divergence and update documentation in the same step.
5.2.1 Lifecycle and ViewModel APIs
collectAsState/collectAsStateWithLifecyclebelongs to:viewcompose-lifecycle-androidxundercom.viewcompose.lifecycle.viewModel/savedStateHandlebelongs to:viewcompose-viewmodel-androidxundercom.viewcompose.viewmodel.- Default host Local injection belongs to the
viewcompose-host-androidbridge and is not duplicated in those modules.
5.3 Root-scoped integration assembly
Normal application roots select integrations explicitly. SPI discovery is reserved for low-level neutral extensions, and reflection is a separately reviewed last resort.
- Activity, Fragment, navigation, and named design-system roots construct their root-scoped overlay host explicitly; classpath order may not select a design system.
AndroidOverlayHostFactoryProvider + ServiceLoaderremains only for custom low-level hosts and discovers exactly one neutralviewcompose-overlay-androidprovider. Material and One UI adapters register no whole-host provider.- Optional decoration uses
AndroidViewDecorationBackend + ServiceLoader; renderer and host do not depend on the shadow implementation, and absence is a no-op. - Temporary reflection includes architecture documentation, contract tests, and a removal plan.
5.4 Local API consistency
- Public Local APIs are
uiLocalOf,UiLocals.current,ProvideLocal, andProvideLocals. - Do not add specialized
ProvideXxxwrappers. - Local mechanism changes add snapshot, Lazy, and overlay propagation regression.
- Converge an old specialized wrapper in the same cycle and update documentation.
5.5 NodeSpec-only semantics
- New semantic fields belong only in
NodeSpecorModifier, never dynamicProps. - Do not add or restore
Props/TypedPropKeys/PropKeys/node.props. - A renderer binder reads an explicit spec and cannot silently fall back to a default spec.
- Additional metadata uses a modifier element or explicit spec field, never an implicit map.
- Update NodeSpec-only and its guards.
5.6 Node-group recomposition stability
- Keep sibling
emitgroup keys and order stable; use explicit keys for loops and conditionals. - If stability is impossible, accept nearest-stable-ancestor fallback and test it.
- Structure drift remains observable; do not suppress warnings or exceptions.
- Changed
emit(spec/modifier)inputs mark the group dirty. - Add a Runtime/UI Foundation test for group reuse and fallback.
5.7 Snapshot consistency
MutableStatewrites use explicitMutableSnapshotor autocommit; no bypass path is allowed.- Mutation equivalence and concurrent merging use
SnapshotMutationPolicy, not scattered caller comparisons. - Test no-conflict, merge-success, and merge-failure cases.
- Test read consistency within one composition pass.
- Update State snapshots with semantic changes.
- During composition, a value written to mirror state and read back cannot control coroutine launch, scheduling, or version selection. Read the live kernel field and add regression coverage.
5.8 Composition transactions and structured coroutines
- Composition uses prepare/commit/abort; renderer failure cannot commit slots, observations, or effects.
DisposableEffect,SideEffect, andLaunchedEffectstart only after commit.- A failed candidate calls
RememberObserver.onAbandoned, notonForgotten. - Application-visible async work belongs to the RenderSession composition Job; no independent
CoroutineScope(SupervisorJob())root. - A custom context can override non-Job elements only; a supplied Job fails fast.
- Coroutine tests cover key restart, conditional removal, failed composition, Session disposal, and child failure isolation.
- Renderer transaction tests cover sibling and recursive failure, new-node release, old View order, and binding restoration.
AndroidView.update/onReset/nativeViewis replay-safe and changes only its View. External non-replayable work uses post-transactiononCommit.- Transaction journals scale with touched scopes and mutated nodes, not the whole tree.
- Fast-path changes verify stable VNode/List identity, SkipSubtree without child traversal, and no deep structure statistics while diagnostics are off.
- Duplicate-invalidation optimization preserves next-frame work created during composition and merges repeated same-frame writes.
- Ordinary Kotlin captures inside
RecomposeBoundaryare declared throughinputs; snapshot state is observed directly. - Every new failure path maps to a structured
RenderFailurestage/recovery result and does not block later commit callbacks or cleanup.
5.9 Frame-aligned scheduling
- State invalidation uses
FrameAlignedRenderDispatcher + Choreographer, notcontainer.post. RenderSession.render()remains immediate unless architecture and tests change first.- Dispatcher tests cover same-frame merge, cancellation, reentrant next-frame work, and cross-thread deduplication.
- Instrumentation that waits for UI idle also waits at least one frame.
- Session disposal guarantees no delayed render afterward.
5.10 Renderer registration single source
NodeType -> binder,NodeViewPatch -> applier, andNodeSpec -> patch factorymappings live only inNodeBinderDescriptors.- Do not add parallel maps in
NodeViewBinderRegistryorNodeBindingDiffer. - Add a descriptor before binder/patch code for a new node.
- Run descriptor guards after changes.
NodeBinder*.ktlives underview/tree/binder/core/descriptor/, not the core root.- Restore a regressed directory and add a structural guard in the same commit.
5.11 Module dependencies
- Register every runtime module exactly once as Kernel, UI Foundation, Android Engine, Design System, Integration, or the explicit consumer aggregate; register Tooling separately.
- Dependencies may point to the same or a lower allowed layer only. Tooling never enters a
published runtime dependency, and no
viewcompose-*module depends onapp. - UI Foundation production sources cannot import Renderer, AndroidX, or Material APIs. UI Contract
production sources cannot import
android.*orandroidx.*. - Neutral
ComponentActivity/Fragment.setUiContentlives inviewcompose-android; namedsetMaterial3UiContentlives inviewcompose-material3-android;renderIntoandAndroidView/nativeViewremain in the low-levelviewcompose-host-androidengine. - Material theme policy lives in
viewcompose-material3, while Material Activity/Fragment and presentation wiring lives only in explicitly named integrations. UI Foundation, Renderer Android, Host Android, and the neutral Android aggregate cannot import or depend on Material. verifyModuleDependencyBoundariesandverifyDesignSystemIsolationinqaQuickare non-waivable gates. Guard tests enforce these boundaries; review convention alone is insufficient.- Classify each published dependency by consumer exposure, not by implementation convenience:
public/protected signature types and intentional entry-point aggregates use
api; dependencies that are fully private to the implementation useimplementation. A caller-owned platform integration is the only exception and must be named in the module manual and external-consumer test; it cannot be inferred from an existingimplementationdeclaration. - A normal application declares the aggregate and optional-feature artifacts it uses. Do not document lower-layer coordinates as mandatory workarounds for incomplete Maven metadata.
- Add every direct ViewCompose publication edge to
gradle/viewcompose-dependency-contracts.propertiesin the same change.verifyViewComposeDependencyContractsrejects drift between that contract and Gradle declarations. - A new or changed entry point must include a minimal external-consumer compile test. Published
repository inspection must preserve
apias Maven compile scope andimplementationas runtime scope before release. - Dependency exposure changes are publication-input changes: update the owning module manual and add immutable release intent in the same pull request.
- Before first Central publication, a repository Maven sample may use a new coordinate only when
its gate first publishes the current checkout to
build/maven-repositoryand then consumes the generated POM. After publication, repeat the installation verification from a clean checkout without the generated repository.
5.12 Development tooling isolation
Development tooling that can execute inside an application process follows ADR-0009:
- Concrete preview, inspector, source-navigation, and IDE transport implementations live only in modules classified as Tooling. Runtime modules may expose nullable neutral ports but cannot own a concrete tooling protocol, report writer, request receiver, or IDE lifecycle.
- Activation requires all three conditions: optional tooling artifact present, debuggable process,
and a valid explicit tool request. Never interpret
debuggableas permission for continuous observation. - Inactive tooling installs no listener on scroll, global layout, draw, touch, animation frame, or recomposition; performs no View traversal, stack capture, serialization, or file I/O; and owns no eagerly started worker. Any narrowly justified exception needs an ADR, allowlist entry, and same-device benchmark evidence.
- Prefer one nonce-bearing request and one response snapshot over continuously refreshed reports. Validate nonce, process, package, size bounds, lifecycle cleanup, stale response rejection, and failure isolation in deterministic tests.
- Runtime changes run
verifyDevelopmentToolingIsolation. Tooling that observes a hot path also compares the same debug build, device, workload, refresh rate, and thermal state. Idle scrolling must produce zero tooling writes. - Pull requests explicitly answer whether application-process tooling code changed, why runtime ownership remains neutral, how release classpath exclusion is proven, and what inactive-path evidence was collected.
5.13 One package root per module
- Each module has one package prefix across main, test, and androidTest.
- Android namespace matches that root, except the documented ui-contract exception.
- Lifecycle and ViewModel public packages remain
com.viewcompose.lifecycleandcom.viewcompose.viewmodelin their owning modules. verifyModulePackageRootsandverifyAndroidModuleNamespacesare non-waivableqaQuickgates.
5.14 Runtime purity and coverage
- runtime remains a Kotlin/JVM module.
- runtime production source cannot import Android/AndroidX or depend on
androidx.core.ktx. verifyRuntimePurityblocks violations inqaQuick.- Policy, snapshot, observation, invalidation, and composer branch changes add unit tests.
5.15 Host session and diagnostics
- Android frame clock and dispatcher implementation lives in host-android; UI Foundation retains
only the
RenderSessionRuntimecontract/provider. - Aggregate
setUiContentand EnginerenderIntoexpose coreRenderStats/RenderTreeResult, never renderer implementation types. - Lazy-item and overlay child sessions use the session contract rather than constructing a platform implementation directly.
- Guards cover renderer-type leakage and provider-missing no-op fallback.
5.16 Modifier and container policy
- ui-contract Modifier contains globally stable semantics, never a policy meaningful to only one container.
reusePolicyandmotionPolicyare container DSL and NodeSpec fields read directly by the renderer. A behavior that is required for correctness, such as native focused-descendant visibility, is an invariant rather than an opt-in policy.- Pager residency and direct-input controls remain explicit container fields. Disabling direct input does not disable state commands or programmatic focus visibility.
- A new policy includes DSL-to-NodeSpec and renderer bind/patch tests. Before adding a Boolean, verify that the behavior is genuinely optional and has one stable owner.
5.17 Developer Preview
- preview-core contains annotations, deterministic configuration, and version protocol without Android/AndroidX imports.
- preview-runner mounts native Views, captures screenshots, and exports diagnostics without Compose or IDE SDK dependencies.
PreviewCatalogin preview is the single source; a component adds aPreviewSpec.- Paparazzi consumes the same catalog and does not maintain separate screenshot examples.
qaPreviewis required; visual changes update baselines and pass protocol/runner/snapshot tests.- Preview modules do not depend on app or import Demo packages.
- Worker and IDE plugin communicate only through structured data with
protocolVersion/requestId. - Preview overlays are static simulations; real windows are instrumentation concerns.
5.18 Animation and gestures
- Animation ownership is animation-core, animation DSL, and host-android interop. Gesture ownership is gesture-core policy, gesture DSL, and renderer Android adaptation.
- TransitionManager, MotionLayout, and Animator are host-android interop only.
- graphicsLayer changes add renderer patch/rebind tests and cannot use full-rebind fallback.
- Gesture consumption is gesture-first then clickable fallback; parent-scroll competition is tested.
- List/Pager motion is opt-in; motion/reuse semantic changes add container tests and docs.
- AnimatedVisibility uses
NodeType.AnimatedVisibilityHost; hidden content is removed after exit. - pointerInput changes test that
Consumedblocks transform, drag, anchored drag, and combined click. - Transform thresholds test pan/zoom/rotation slop plus two-pointer instrumentation.
- Anchored settle tests velocity, distance, and nearest-anchor paths.
updateTransitionmaintains one shared timeline across channels; AnimatedVisibility reuses it.animateContentSizeremains a layout transition observable by the parent, not a visual scale.- AnimatedSizeHost tests smooth expansion and collapse.
- Axis lock, transform slop, and swipe settle algorithms live in gesture-core; renderer adapts thresholds and events only.
- Enabled
combinedClickablewith no callbacks remains a no-op and does not consume input.
5.19 ConstraintLayout
- DSL/scope lives in widget-constraintlayout; renderer maps to Android ConstraintLayout.
layoutId/constrainAs/constrainis parent data; a wrong parent emitsModifierParentDataValidatorwarning.- Inline constraints override a decoupled ConstraintSet for the same child and warn once.
- ConstraintDimension overrides Modifier width/height/size.
- A new helper or ConstraintSet semantic adds DSL unit, renderer unit, and Demo UI anchors.
Barrier(allowsGoneWidgets)must affect rendering.- Chain weights/reference size mismatch fails in DSL and warns once in renderer.
- New min/max/percent/constrained, baseline extension, or circle semantics add DSL emission and renderer application assertions.
5.20 Graphics
- graphics-core contains platform-neutral models and commands without Android/AndroidX.
- graphics contains Canvas and drawing DSL without Android Canvas execution.
- Renderer alone maps commands to Android Canvas/Paint/Path and patches.
- drawWithCache changes test cache hits and invalidation; per-frame rebuild is prohibited.
- RenderEffect, RuntimeShader, and Drawable bridges are host-android interop only.
- Visual semantic changes update PreviewCatalog and Paparazzi under the
qaPreviewgate.
6. Interrupted-task recovery
Recover in this order:
git log;- current
git diff; - current roadmap and architecture documents;
- the latest failed log or test report;
- conversation memory last.
Repository state is the project context of record; a chat thread is not.
7. Commit messages
The subject describes one minimum step. Use a direct docs:, feat:, fix:, test:, or
refactor: subject, for example feat: add overlay host contract or
fix: refresh dialog content on state updates.
8. Default execution order
Plan, implement the minimum step, commit it when complete, and continue to the next step. The goal is not commit count; it is reviewable, reversible, and recoverable progress.
9. Documentation layers
The documentation governance standard owns complete placement, naming, linking, and lifecycle rules.
- Current entry:
docs/README.md - Long-lived standards: architecture, guides, tooling, and project
- Cross-session execution:
docs/project/plans/ - Historical evidence:
docs/archive/
The repository root contains only project entry and community governance files.
10. Durable execution plans
For work spanning multiple steps or days:
- create a lowercase kebab-case plan under
docs/project/plans/; - after each completed step and commit, update its checklist and execution log;
- record baseline, completion condition, remaining work, and next step;
- after completion, write durable conclusions into current documents and move the plan to archive;
- close stale in-progress, incomplete, next, and pending markers in the roadmap and related current documents.