Add AI agent workflows and guardrails for code development - #2822
Add AI agent workflows and guardrails for code development#2822MarkYav wants to merge 3 commits into
Conversation
…ment. ### Motivation This introduces strict machine-readable rules and specialized subagents to guide AI-assisted tasks. By formalizing these operational guardrails, we ensure that coding agents adhere to the team's and project's coding preferences, and maintain uniform documentation quality without relying on manual intervention. ### Technical Context - Routing & Catalog (`AGENTS.md`): Points agents to canonical Lottie specifications and acts as the entry point for all workflows. - Strict TDD Enforcement (`dev-test.md`): Adds an isolated `dev-test` subagent tasked with generating black-box functional tests prior to any feature implementation. - Execution & Git Discipline: Institutionalizes atomic commits and splits functional/non-functional work via `commit_rules.md`. - Testing & Review Criteria: Sets explicit expectations for code reviews, code documentation, and module-specific Gradle execution orders natively comprehensible to AI assistants.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
For the given class internal object BaseVectorPropertySerializer :
JsonContentPolymorphicSerializer<BaseVectorProperty>(BaseVectorProperty::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<BaseVectorProperty> {
val obj = element as? JsonObject ?: throw SerializationException("Expected JSON object")
val animated = obj["a"]?.jsonPrimitive?.intOrNull
return when (animated) {
1 -> AnimatedVectorProperty.serializer()
0 -> StaticVectorProperty.serializer()
null -> throw SerializationException("Vector property missing required 'a' field per Lottie schema")
else -> throw SerializationException("Field 'a' must be 0 or 1, but was $animated")
}
}
}Generated comment before: /** Polymorphic serializer for [BaseVectorProperty] based on "a" field. */And after: /**
* Polymorphic serializer for [BaseVectorProperty] discriminating between static and animated
* variants based on the Lottie schema `"a"` field ([Integer
* Boolean](https://lottie.github.io/lottie-spec/dev/specs/values/#int-boolean)).
*
* Contract:
* - Preconditions: [element] must be a [JsonObject].
* - Postconditions:
* - Selects [AnimatedVectorProperty.serializer] when `"a"` is integer `1`.
* - Selects [StaticVectorProperty.serializer] when `"a"` is integer `0`.
* - Exceptions:
* - Throws [SerializationException] if [element] is not a [JsonObject].
* - Throws [SerializationException] if `"a"` is missing.
* - Throws [SerializationException] if `"a"` is neither `0` nor `1`.
*/ |
|
Prompt (not tuned): Explore the Lottie specification for the vector property, and generate corresponding tests to cover the specifications. Model: Gemini 3.8 Flash Generated report (click to open):Lottie Vector Property: Specification Analysis & Test PlanCanonical Specification Reference
1. Domain Structuring & Equivalence PartitioningDomain A: Format AST & Deserialization (
|
| Parameter | Equivalence Class | Selected Boundary / Concrete Values |
|---|---|---|
a (Animated Flag) |
EC-A1 (Valid Default): Omitted (implicit 0) EC-A2 (Valid Static): 0EC-A3 (Valid Animated): 1EC-A4 (Invalid): 2, -1, "0", {}
|
null (omitted), 0, 1, 2, "true"
|
sid (Slot ID) |
EC-SID1 (Valid Absent): Omitted / nullEC-SID2 (Valid Non-empty): String identifier EC-SID3 (Valid Empty): Empty string ""
|
null, "vector.scale", ""
|
k (Static Vector) |
EC-SK1 (Nominal 2D): Standard 2D vector EC-SK2 (Boundary 1D): Single component scalar vector EC-SK3 (Boundary 3D): Standard 3D vector EC-SK4 (Boundary 4D): 4-component vector EC-SK5 (Boundary 0D): Empty array []EC-SK6 (Numeric Extremes): Negative, zero, integer-literal, decimals EC-SK7 (Invalid Types): Primitive number, object, string, non-numeric elements |
[100.0, 200.0], [42.0], [1.0, 2.0, 3.0], [1.0, 0.5, 0.2, 1.0], [], [0.0, -15.5, 100.0], 10.5, ["invalid"]
|
k (Animated Keyframes) |
EC-AK1 (Nominal Multi-KF): EC-AK2 (Boundary 1-KF): EC-AK3 (Boundary 0-KF): [])EC-AK4 (Invalid): Missing field, array of non-objects |
2 keyframes, 3 keyframes, 1 keyframe, [], {}
|
t (Frame Time) |
EC-KT1 (Nominal): Positive float EC-KT2 (Boundary Zero): 0.0EC-KT3 (Default Absent): Omitted (defaults to 0.0) EC-KT4 (Fractional): Sub-frame time EC-KT5 (Negative): Timeline before zero |
10.0, 0.0, omitted, 14.5, -5.0
|
s (Start Value) |
EC-KS1 (Nominal): Dimension-matching float array EC-KS2 (Boundary Empty): []EC-KS3 (Invalid): Missing s, non-array |
[1.0, 2.0], [], null/missing, "abc"
|
h (Hold Flag) |
EC-KH1 (Default Absent): Omitted (defaults to 0 / false) EC-KH2 (Int Zero): 0 (false)EC-KH3 (Int One): 1 (true)EC-KH4 (Bool False): falseEC-KH5 (Bool True): trueEC-KH6 (Invalid): Non-boolean/int primitive |
omitted, 0, 1, false, true, "invalid"
|
i & o (Easing Tangents) |
EC-KE1 (Default Absent): Omitted (linear) EC-KE2 (Nominal Scalar): {"x": 0.4, "y": 0.8}EC-KE3 (Array Format): {"x": [0.4], "y": [0.8]}EC-KE4 (Extremes): Clamped limits 0.0 and 1.0EC-KE5 (Invalid): Non-numeric, missing coords |
omitted, {"x": 0.4, "y": 0.8}, {"x": [0.4], "y": [0.8]}, {"x": 0.0, "y": 1.0}, {"x": "bad"}
|
Domain B: Animation & Evaluation Semantics (VEC-ANIM)
| Dimension | Equivalence Class | Selected Boundary / Concrete Values |
|---|---|---|
| Vector Dimensionality ( |
EC-VD1: 1D ([X])EC-VD2: 2D ( [X, Y])EC-VD3: 3D ( [X, Y, Z])EC-VD4: 4D ( [R, G, B, A])EC-VD5: 0D ( []) |
1D: [50f], 2D: [100f, 200f], 3D: [1f, 2f, 3f], 4D: [1f, 0.5f, 0.2f, 1f], 0D: floatArrayOf()
|
| Keyframe Topology |
EC-TP1: Single Keyframe ( EC-TP2: Two Keyframes starting at EC-TP3: Delayed Start ( EC-TP4: Multi-segment Sequence ( EC-TP5: Coincident Keyframes ( |
|
| Evaluation Temporal Point ( |
EC-TE1: Pre-animation underflow ( EC-TE2: Exact start boundary ( EC-TE3: Intermediate progress ( EC-TE4: Exact end boundary ( EC-TE5: Post-animation overflow ( |
|
| Interpolation Mode |
EC-IM1: Linear default (tangents absent) EC-IM2: Cubic Bézier easing EC-IM3: Hold step ( EC-IM4: Mixed chaining (e.g. Seg 1 Hold, Seg 2 Bézier) |
Linear (0,0)->(1,1); S-Curve (0.4, 0.0)->(0.2, 1.0); Hold [Hold -> Bezier]
|
2. Semantic Constraints & Pruning Rules
-
Static Exclusivity: When
a = 0(or omitted), keyframes do not exist. Evaluation is invariant across all query frames$t$ . -
Single Keyframe Invariance: When
$N = 1$ , interpolation modes (h,i,o) are inactive because there is no target frame. Value is strictly$kf_0.s$ for all$t$ . -
Hold Precedence: When
$h = 1$ (ortrue), Bézier control tangents$i$ and$o$ are suppressed; the value is held constant at$kf_i.s$ across$[t_i, t_{i+1})$ . - Dimensional Consistency: In valid animations, all keyframes share identical vector dimensionality ($length(kf_0.s) == length(kf_i.s)$).
-
Coincident Segment Bound: For zero-duration segments where
$t_i = t_{i+1}$ , evaluation at$t \ge t_{i+1}$ takes$kf_{i+1}.s$ without division by zero.
3. Pairwise Combinatorial Test Grid
Table 1: Deserialization / AST Tests (VEC-PARSE)
| Test ID | Property Type | Input Fields & Boundary Setup | Verification Assertions |
|---|---|---|---|
VEC-PARSE-001 |
Static | {"a": 0, "k": [10.0, 20.0]} |
StaticVectorProperty, animated == false, value == [10f, 20f], slotId == null
|
VEC-PARSE-002 |
Static |
{"sid": "vec.scale", "k": [1.0, 2.0, 3.0]} (omitted a) |
StaticVectorProperty, animated == false, slotId == "vec.scale", 3D array |
VEC-PARSE-003 |
Static | {"a": 0, "k": [42.5]} |
StaticVectorProperty, 1D float array |
VEC-PARSE-004 |
Static | {"a": 0, "k": []} |
StaticVectorProperty, empty array FloatArray(0)
|
VEC-PARSE-005 |
Static | {"a": 0, "k": [0, -15.5, 1000]} |
Correctly parses zero, negative float, and integer literal into floats |
VEC-PARSE-006 |
Static | {"a": 0, "k": 10.5} |
Throws SerializationException (non-array scalar) |
VEC-PARSE-007 |
Static | {"a": 0, "k": ["invalid"]} |
Throws SerializationException (non-numeric element) |
VEC-PARSE-008 |
Animated | {"a": 1, "k": [{"t": 0, "s": [1.0, 2.0]}, {"t": 10, "s": [3.0, 4.0]}]} |
AnimatedVectorProperty, animated == true, 2 keyframes |
VEC-PARSE-009 |
Animated | {"a": 1, "k": [{"t": 0, "s": [10.0]}]} |
Single keyframe ( |
VEC-PARSE-010 |
Animated | {"a": 1, "k": []} |
Empty keyframes list |
VEC-PARSE-011 |
Animated | Keyframe with "h": 1 and "h": 0 (int-boolean) |
Correctly parses hold == true and hold == false
|
VEC-PARSE-012 |
Animated | Keyframe with "h": true and "h": false (boolean) |
Correctly parses hold == true and hold == false
|
VEC-PARSE-013 |
Animated | Keyframe with omitted "h"
|
Defaults to hold == false
|
VEC-PARSE-014 |
Animated | Keyframe with scalar easing tangents: "i": {"x": 0.2, "y": 0.8}
|
ScalarKeyframeEasing(0.2f, 0.8f) |
VEC-PARSE-015 |
Animated | Keyframe with array easing tangents: "i": {"x": [0.2], "y": [0.8]}
|
ScalarKeyframeEasing(0.2f, 0.8f) |
VEC-PARSE-016 |
Animated | Keyframe with omitted tangents | Tangents deserialize to null
|
VEC-PARSE-017 |
Animated | Keyframe with fractional frame time "t": 12.5
|
frame == 12.5f |
VEC-PARSE-018 |
Animated | Keyframe with omitted frame time "t"
|
Defaults to frame == 0f
|
VEC-PARSE-019 |
Animated | Keyframe with negative frame time "t": -5.0
|
frame == -5f |
VEC-PARSE-020 |
Animated | Keyframe missing required "s"
|
Throws SerializationException
|
VEC-PARSE-021 |
Polymorphic | Round-trip serialization of StaticVectorProperty
|
Re-deserialized object equals original |
VEC-PARSE-022 |
Polymorphic | Round-trip serialization of AnimatedVectorProperty
|
Re-deserialized object equals original |
Table 2: Animation Evaluation Tests (VEC-ANIM)
| Test ID | Dimensionality | Topology & Easing | Query Point ( |
Expected Outcome |
|---|---|---|---|---|
VEC-ANIM-001 |
3D | Static [1f, 2f, 3f]
|
Returns [1f, 2f, 3f] at all frame times |
|
VEC-ANIM-002 |
2D | Single keyframe at [10f, 20f]
|
Returns [10f, 20f] at all frame times |
|
VEC-ANIM-003 |
3D | Two keyframes [0, 10, 20] -> [10, 20, 30]
|
Exact start: |
Returns [0f, 10f, 20f]
|
VEC-ANIM-004 |
3D | Two keyframes [0, 10, 20] -> [10, 20, 30]
|
Midpoint: |
Returns arithmetic mean [5f, 15f, 25f]
|
VEC-ANIM-005 |
3D | Two keyframes [0, 10, 20] -> [10, 20, 30]
|
Exact end: |
Returns [10f, 20f, 30f]
|
VEC-ANIM-006 |
3D | Two keyframes [0, 10, 20] -> [10, 20, 30]
|
Overflow: |
Clamps and holds [10f, 20f, 30f]
|
VEC-ANIM-007 |
2D | Delayed start [100f, 200f] -> [300f, 400f]
|
Underflow: |
Holds initial keyframe [100f, 200f]
|
VEC-ANIM-008 |
2D | Hold keyframe ([10f, 20f] -> [30f, 40f]
|
|
At [10f, 20f]. At [30f, 40f]
|
VEC-ANIM-009 |
2D | Hold keyframe ([10f, 20f] -> [30f, 40f]
|
|
At [10f, 20f]. At [30f, 40f]
|
VEC-ANIM-010 |
2D | Cubic Bézier easing ($o=(0.4, 0.0), i=(0.2, 1.0)$) [0f, 0f] -> [100f, 100f]
|
|
Non-linear S-curve evaluated value |
VEC-ANIM-011 |
2D | 3-keyframe sequence [0, 0] -> [10, 20] -> [30, 40]
|
|
At [5f, 10f]. At [20f, 30f]
|
VEC-ANIM-012 |
2D | Mixed: Seg 1 Hold ( |
At kf_0. At kf_1. At (kf_1 + kf_2)/2
|
|
VEC-ANIM-013 |
2D | Mixed: Seg 1 Bézier, Seg 2 Hold ( |
At kf_1
|
|
VEC-ANIM-014 |
1D | 1D vector interpolation: [100f] -> [200f] over |
Returns 1D list [150f]
|
|
VEC-ANIM-015 |
2D | Inverse directions: 0->100, 200->50
|
|
|
VEC-ANIM-016 |
4D | 4-vector interpolation: [1, 0, 0.5, 1] -> [0, 1, 0.5, 0]
|
[0.5f, 0.5f, 0.5f, 0.5f] |
|
VEC-ANIM-017 |
2D | Coincident keyframes [10f, 20f] -> [30f, 40f]
|
Evaluates cleanly to [30f, 40f] without NaN / div-by-zero |
|
VEC-ANIM-018 |
2D | Negative query time |
Holds start value kf_0.s
|
|
VEC-ANIM-019 |
2D | Negative timeline keyframes [-50f, 0f] -> [50f, 100f]
|
|
Evaluates arithmetic midpoint [0f, 50f] crossing |
VEC-ANIM-020 |
2D | Extreme magnitude floats ( |
|
Precision preserved without float overflow |
Example of a generated test (bug caught):
/**
* [VEC-PARSE-011] Deserializes keyframe hold flag represented as integer-boolean `h = 1` and `h =
* 0`.
*
* Root cause: Specification requires `h` in
* [Lottie Integer Boolean](https://lottie.github.io/lottie-spec/1.0.1/specs/values/#int-boolean)
* to accept integer boolean `0` or `1`. VectorPropertyKeyframe currently relies on the default
* boolean serializer which rejects integers with JsonDecodingException ("Expected boolean, but
* had 1"). A custom serializer supporting both boolean and int-boolean (similar to
* ScalarPropertyKeyframeSerializer) is required in production code.
*
* Specification:
* [Lottie Integer Boolean](https://lottie.github.io/lottie-spec/1.0.1/specs/values/#int-boolean)
*/
@Ignore(
"BUG: VEC-PARSE-011: VectorPropertyKeyframe does not deserialize int-boolean (0/1) for hold property 'h'"
)
@Test
fun deserializesVectorKeyframe_whenHoldFlagIsIntegerBoolean() {
val jsonHoldTrue = """{"t": 0, "h": 1, "s": [1.0, 2.0]}"""
val keyframeTrue =
LottieDecoder.json.decodeFromString(VectorPropertyKeyframe.serializer(), jsonHoldTrue)
assertThat(keyframeTrue.hold).isTrue()
val jsonHoldFalse = """{"t": 0, "h": 0, "s": [1.0, 2.0]}"""
val keyframeFalse =
LottieDecoder.json.decodeFromString(VectorPropertyKeyframe.serializer(), jsonHoldFalse)
assertThat(keyframeFalse.hold).isFalse()
}|
During the tests construction, the agent generates a report (example of the report is attached to #2822 (comment)) and the question is if we should include it in the codebase or not. There is an option to
I personally use Spec-Driven Development (SDD) a lot, and I have a separate (nested) folder for the artifacts. This aligns very well with the artifacts, generated by this agent. But they don't go into the production code. After certain threshold, I start tracking them using git as a separate repository. |
yschimke
left a comment
There was a problem hiding this comment.
I think we should start off small, and make maybe something 1/5 the size part of the general horologist guidance. For Lottie maybe focus on the spec and how to test against the lottie spec.
comparing lottie guidance to horologist then we should either
- make it general horologist guidance
- question why lottie differs
| @@ -0,0 +1,85 @@ | |||
| --- | |||
| name: dev-test | |||
There was a problem hiding this comment.
This feels like there is a whitepaper behind it, but I'm not convinced being this prescriptive will help in two ways
- friction depending on the agents used, overall introduces a lot of process for something simple.
- if this is best practice as of September 2026, how do we know it will continue.
|
|
||
| --- | ||
|
|
||
| ## VIOLATION DETECTED (SELF-CORRECTION TRAP) |
There was a problem hiding this comment.
This feels like something a harness should do, not guidance in every agent file.
| subagent: true | ||
| --- | ||
|
|
||
| # Dev-Test: Stringent Functional Testing Generator |
There was a problem hiding this comment.
I'm not sure all horologist tests need to be stringent like this. White box testing can have a place, and sometimes code is exploratory, and not worth extensive tests initially.
| # RemoteCompose Lottie Guidelines | ||
|
|
||
| <lottie_specifications> | ||
| ## Canonical Lottie Specifications |
| description: "Guidelines and template for drafting Pull Request descriptions and checklists." | ||
| --- | ||
|
|
||
| # Pull Request Guidelines |
There was a problem hiding this comment.
This could just be a link to the actual template
There was a problem hiding this comment.
Good idea! Should we move this rule to the root, so it affects all the agents that are working on Horologist, not only on the Lottie module?
There was a problem hiding this comment.
Yeah, let's move it to the root.
|
|
||
| ### A. The Call (Preconditions & Input State) | ||
| Document all constraints required for the caller to invoke the method safely: | ||
| - **Nullability:** Explicitly declare whether arguments can be null (`@NonNull`, `@Nullable`). |
There was a problem hiding this comment.
this seems to be java related, not a think in Kotlin?
| - **Value Bounds:** Specify ranges (e.g., `offset >= 0`, `len >= 0`). | ||
| - **Relational Constraints:** Specify cross-parameter relationships (e.g., `offset + len <= cbuf.length`). | ||
|
|
||
| ### B. The Results (Postconditions & Output State) |
There was a problem hiding this comment.
This feels heavyweight and prescriptive and may be confused with Compose code.
| 3. Error Hotspots: `null`, `[]`, empty strings, missing fields, 32-bit/64-bit limits, boundary±1 values, floating-point precision/epsilon. | ||
| 4. Domain Anomalies: Degenerate geometry (e.g., $<2$ points in Bézier, out-of-range colors, invalid easing tangents, inverted keyframe time). | ||
|
|
||
| ## STATE 3: COMBINATORIAL SELECTION & PAIRWISE GRID |
There was a problem hiding this comment.
I don't think this matches what we do in Horologist, for any UI components we tend to focus on screenshot tests of key states, and avoid explosions of values tested.
### Impact Summary Simplifies the AI coding assistant instructions for `:remotecompose:lottie` by eliminating overly prescriptive rule files. This makes the instructions more generic and better aligns pull with existing Horologist repository standards. ### External References - Horologist Pull Request Template: https://github.com/google/horologist/blob/main/. github/pull_request_template.md TAG=agy CONV=acc9e3d7-2a7e-4c05-8437-7e64d07b6298
|
Reduced the size of the instructions to target Lottie spec, commits and PRs. |
| - Information that is obvious from reading the code. | ||
| - Code maintenance instructions (place these in code comments instead). | ||
| - Short-term discussions. | ||
| - Preview URLs and build artifacts. |
There was a problem hiding this comment.
Why are preview URLs banned?
|
|
||
| ## 6. Scope and Isolation | ||
| - **DO NOT** mix functional and non-functional changes in the same commit. If you are asked to add a feature, do not simultaneously reformat surrounding code, reorganize imports, or refactor unrelated methods in the same commit. | ||
| - Keep commits atomic (narrowly scoped): Split into **Commit A (Refactor/Cleanup)** and **Commit B (Feature Logic)** when preparing code for a new feature. |
There was a problem hiding this comment.
is this about commits? or PRs?
| - Preview URLs and build artifacts. | ||
|
|
||
| ## 6. Scope and Isolation | ||
| - **DO NOT** mix functional and non-functional changes in the same commit. If you are asked to add a feature, do not simultaneously reformat surrounding code, reorganize imports, or refactor unrelated methods in the same commit. |
There was a problem hiding this comment.
Can we weaken DO NOT, to PREFER to separate...?
WHAT
This PR introduces rules and specialized sub-agents to guide AI coding agents.
WHY
By formalizing these operational guardrails, we ensure that coding agents adhere to the team's and project's coding preferences, and maintain uniform documentation quality without relying on manual intervention. This is especially critical for open-source projects.
As the first step, these changes are specific to
:remotecompose:lottiemodule, but can be extended to the whole Horologist repository.HOW
AGENTS.md): Points agents to canonical Lottie specifications and acts as the entry point for all workflows.dev-test.md): Adds an isolateddev-testsubagent tasked with generating black-box functional tests prior to any feature implementation.commit_rules.md.Checklist 📋