| name | a9s-add-attention-column |
| description | Recipe for adding a list-view attention column to a resource type โ handles Tier A (existing field), Tier B (computed/Wave-2), and the FieldUpdates promotion path. Optimized to avoid the rediscovery overhead that plagued the first Tier A sweep. |
| disable-model-invocation | true |
Adding an Attention Column
The user wants WHY a row is colored visible in the list โ not just that it's colored. This skill standardizes the recipe so adding columns is mechanical, not a per-type re-derivation.
Prerequisites
You MUST have a scoped task from the architect with:
- ShortName (e.g.
kms, redis, cfn)
- Column title (e.g.
Rotation, Failover, Drift)
- Source category (one of: A, B-fetcher, B-enricher, C-detail-only)
- Field key OR struct path for the value
- Optional: format/decorator (when raw value isn't user-friendly)
- AWS docs reference (the
attention-signals.md row) for the contract
If you don't have this, STOP. Reply with REJECTED and ask for architect scope.
Source category decision
Is the value already on the SDK list-API response (RawStruct path)?
โโ YES โ Tier A: add column with Path: in defaults_*.go
โโ NO โ
Does the fetcher already write it to Fields[]?
โโ YES โ Tier A: add column with Key: in defaults_*.go
โโ NO โ
Can it be computed cheaply per-row at fetch time (Wave-1)?
โโ YES โ Tier B-fetcher: edit fetcher to write Fields[<key>], then add column with Key:
โโ NO โ
Does it require an additional AWS API call?
โโ YES โ Tier B-enricher:
โ โโ Edit/add Wave-2 enricher to populate
โ โ IssueEnricherResult.FieldUpdates[resourceID][<key>]
โ โโ Wire via the catalog literal's Wave2 + IssueEnricherFieldKeys
โ โ fields in core/aws/catalog_<category>.go
โ โโ Add column with Key: in defaults_*.go
โโ Multi-line text body? โ Tier C: detail-only via DetailField{Key: ..., Label: ...}
Pre-flight checklist (architect does this BEFORE dispatching)
The architect's job is to eliminate per-step rediscovery. Provide:
- Exact file paths the agent will edit
- Exact Edit operations โ
old_string / new_string snippets, not prose
- SDK enum constants verified via
go doc <pkg>.<Type> โ not guessed
- Existing fake/fixture pattern โ link a sibling test for the agent to mirror
- One worked example in the same file the agent will edit
Tier A โ Path or Key already populated
File: core/config/defaults_<group>.go
Find the <shortName> entry's List []ListColumn slice. Insert the new column AFTER the primary status column:
{Title: "<Title>", Path: "<SDKPath>", Width: <N>},
{Title: "<Title>", Key: "<field_key>", Width: <N>},
Then
cd /Users/k2m30/projects/a9s
go run ./cmd/viewsgen/
make build && make lint && make test
Test
The architect dispatches a9s-qa with a test that asserts the COLUMN HAS DATA, not that the column exists. A column-existence test is busywork; a "value is non-empty for a fixture that should trigger it" test catches real wiring breakage. Example:
func TestFetch<Type>_<Field>_Populated(t *testing.T) {
fake := <typeFake>{ }
resources, err := awsclient.Fetch<Type>(ctx, fake)
}
Tier B-fetcher โ Wave-1 computed at fetch time
File: core/aws/<short>.go
Find the resource construction site. Add the computed field to the Fields: literal:
Fields: map[string]string{
"<key>": <computed_value>,
},
Add <key> to the type's FieldKeys slice on its catalog literal (core/aws/catalog_<category>.go).
Then defaults_*.go and viewsgen as Tier A.
Test
Test asserts the COMPUTATION:
func TestFetch<Type>_<Field>_ComputesCorrectly(t *testing.T) {
}
Don't test the trivial round-trip; test the logic.
Tier B-enricher โ Wave-2 via FieldUpdates
File: core/aws/<short>_issue_enrichment.go
Types with Wave 2 signals have an _issue_enrichment.go file; a type with no Wave 2 signal simply omits the Wave2 field on its catalog literal โ create the file if it's missing.
Mirror the existing EnrichDynamoDBPITR / EnrichKMSRotation / EnrichRedisReplicationGroup pattern. Wiring is declarative on the catalog literal in core/aws/catalog_<category>.go (no init(), no register* calls):
Wave2: IssueEnricher{Fn: Enrich<Name>, Priority: 100},
IssueEnricherFieldKeys: []string{"<key>"},
package aws
func Enrich<Name>(ctx context.Context, clients *ServiceClients, resources []resource.Resource, _ resource.ResourceCache) (IssueEnricherResult, error) {
findings := make(map[string][]domain.Finding)
fieldUpdates := make(map[string]map[string]string)
if clients.<Service> == nil {
return IssueEnricherResult{Findings: findings}, nil
}
known := make(map[string]struct{}, len(resources))
for _, r := range resources {
known[r.ID] = struct{}{}
}
for {
if _, ok := known[id]; !ok {
continue
}
fieldUpdates[id] = map[string]string{
"<key>": <value>,
}
}
return IssueEnricherResult{
IssueCount: <real-count or 0 for ~ findings>,
Truncated: <bool>,
Findings: findings,
FieldUpdates: fieldUpdates,
}, nil
}
If the enricher walks paginated results (e.g. ListPackages, ListSubscriptionsByTopic), follow NextToken to the end. Don't len(out.Page) โ that under-counts.
Wire via the catalog literal
Set Wave2: IssueEnricher{Fn: <fn>, Priority: <priority>} on the type's ResourceTypeDef literal in core/aws/catalog_<category>.go. Exactly one Wave2 per type โ the field is the registration; there is no init()/register* path. Wave2EnricherFor(shortName) (core/aws/wave2.go) resolves it; tests/unit/architecture_conformance_test.go pins that every declared Wave2 resolves.
Then defaults_*.go and viewsgen as Tier A.
Test
Two assertions:
result.FieldUpdates[<id>][<key>] == <expected> for a fixture that triggers it
- For paginated enrichers:
fake.calls >= 2 to prove pagination is followed
Tier C โ Detail-only
For multi-line / verbose data:
detail:
- { key: <field_key>, label: "<Label>" }
Or in defaults_*.go:
Detail: []DetailField{
{Key: "<field_key>", Label: "<Label>"},
},
The detail renderer reads Resource.Fields[<key>] at render time. The label appears as the row key.
Pitfalls (from the first Tier A sweep)
-
Dead columns: don't add a Path: for a field that isn't on the fetcher's RawStruct. The column will render blank for every row. The redis Failover bug took 3 rounds to spot. Verify with go doc before writing the column.
-
24h-cutoff vs last-status: enrichers that gate findings on a time window (e.g. backup last 24h) must NOT also gate FieldUpdates on the same window. The "last status" column should reflect the newest job regardless of age.
-
Truncation truncation: len(out.Page1) is wrong when NextToken != nil. Always paginate before counting.
-
Negative truncation in date math: int(time.Until(past).Hours()/24) is 0, not -1. Compare timestamps directly: if !future.After(now) { return "expired" }.
-
Enum case: SDK enums are string types โ string(enum) gives the wire value (e.g. "enabled"). Lowercase explicitly if you want to compare.
-
Test fakes need to embed the aggregate API: type myFake struct { awsclient.SNSAPI } โ then override only the methods you exercise. Without embedding, the compiler complains about missing methods.
-
Don't write column-existence tests: asserting a column exists in defaults that you just added is tautological. Test the value pipeline instead (fixture โ fetcher โ Fields[] โ column).
Verification
cd /Users/k2m30/projects/a9s
go run ./cmd/viewsgen/
make build && make lint && make test 2>&1 | tail -10
ALL must pass. If expectedConfigColumnCounts in tests/unit/qa_name_column_first_test.go mentions <short>, increment its value.
Skip rules
- Don't refactor the fetcher just to enable a column โ use the FieldUpdates path instead. The redis Failover column shipped via Wave-2 enricher rather than fetcher refactor; same pattern applies elsewhere.
- Don't add a column whose data the fetcher genuinely doesn't have AND no Wave-2 path exists. That's a future-work item; document in the defaults_*.go file with a comment indicating the prerequisite.