Civic Translator Infographic Pipeline Artifact Gate Recommendations
Current Flow
The current flow:
- Scan S3 for packet-summary.png / meeting-summary.png
- reconcile-infographic-candidate
- Check whether an infographicPublicationCandidate already exists by imageS3Key
- If it exists, skip candidate creation
The problem: that only skips this reconciliation step. It does not know:
- this infographic was already approved
- it was already published
- the source packet/transcript has not changed
- the layout/render should never run again
Add a Publication State Gate Before Any Regeneration
Your candidate table already has reviewStatus. Instead of only checking:
existingKeys.has(pngKey)
query:
TypeScript
select: {
imageS3Key: true,
reviewStatus: true,
publishedAt: true,
}
Then:
TypeScript
const existingCandidate = existing.find(
x => x.imageS3Key === pngKey
);
if (
existingCandidate?.reviewStatus === "APPROVED" ||
existingCandidate?.publishedAt
) {
console.log(Immutable published infographic: ${pngKey});
continue;
}
Once published, it becomes read-only.
Add a Content Hash, Not Just an S3 Key
The current identity is:
state/body/org/date/file.png
That only tells you where the artifact lives.
You need:
source_hash
↓
validated facts hash
↓
story-plan hash
↓
layout hash
↓
render hash
Example:
JSON
{
"meeting": "ankeny-community-schools-2026-07-06",
"source_hash": "abc123",
"layout_hash": "def456",
"render_hash": "ghi789",
"status": "PUBLISHED"
}
Then:
same source_hash
+
same layout_hash
+
published
skip everything
Add a Fast Path at the Top of Phase 80/81
Before:
packet
↓
facts
↓
plan
↓
editorial
↓
layout
↓
render
Do:
lookup artifact manifest
If:
published=true
source_hash unchanged
return existing artifact.
Something like:
TypeScript
if (artifact.status === "PUBLISHED" &&
artifact.sourceHash === currentSourceHash) {
console.log(
"Skipping published artifact - no source changes"
);
return artifact;
}
Make the S3 Scan Ignore Published Folders
Right now every PNG becomes a candidate because:
TypeScript
if (
object.Key.endsWith("packet-summary.png") ||
object.Key.endsWith("meeting-summary.png")
)
You need a manifest check.
Example:
/iowa/school-boards/ankeny/2026-07-06/
packet-summary.png
artifact-manifest.json
Then:
TypeScript
if (manifest.status === "PUBLISHED") {
continue;
}
Separate Repair Mode from Production Mode
Right now the same script appears to do:
- discovery
- repair
- candidate creation
- missing artifact recovery
Those should be separate commands:
Bash
npm run infographic:discover
npm run infographic:repair
npm run infographic:publish-sync
Production should never repair automatically.
The Ideal Flow
For a meeting:
New meeting detected
|
v
Generate infographic
|
v
Review
|
v
APPROVED
|
v
PUBLISHED
|
v
LOCKED FOREVER
Later scans:
Found PNG
|
v
Is published?
|
YES
|
skip
Only rerun if:
- source packet changed
- transcript changed
- human explicitly requests regeneration
- template version changed
Given your earlier Civic Translator pipeline, the infographic pipeline should consume the same immutable manifest pattern.
The biggest immediate win is probably:
- add publishedAt/status check
- add source_hash
- make Phase 80/81 exit immediately when the artifact manifest says PUBLISHED
That should eliminate 90%+ of the unnecessary iterations.