Artifact Staleness and Routing¶
Purpose¶
This document explains how the control plane tracks artifact family staleness and uses it to route refinement requests to the minimal necessary workflow sequence rather than always triggering a full rebuild.
Related documents:
Typed Artifact Chain¶
Each build stage outputs a typed artifact that the next stage consumes as an authoritative input — not a suggestion to reinterpret from prose:
ValueEngine → ConceptBlueprint (typed)
↓ surface_candidate_hints[] → DesignDocs surface_map (authoritative)
↓ brand_intent → experience_spec.brand_direction
ThemeCapture → CapturedThemeConfig (typed)
↓ theme.variant / appearance → experience_spec.brand_direction
↓ identity.tagline → brand posture
DesignDocs → ExperienceSpec + surface_map + database_intent_bundle (typed)
↓ experience_spec.pages[] → AppPlanAgent page list (authoritative)
↓ surface_map → AppGenerator module generation
AgentGenerator → workflow_bundle
AppGenerator → app_bundle (DRAFT → reviewed → CURRENT)
This chain is only as strong as its weakest typed handoff. Every typed artifact variable in a downstream workflow's context_variables.yaml must declare a data_reference source pointing at the upstream collection and the specific typed field — not at a prose string.
Artifact Families and Their Dependencies¶
The build pipeline produces six artifact families. Each family depends on upstream families:
concept
├── brand (depends on concept)
├── design_docs (depends on concept)
└── experience_spec (depends on concept and design_docs)
└── workflow_bundle (depends on design_docs)
└── app_bundle (depends on design_docs, experience_spec, workflow_bundle, brand)
This dependency order is declared as artifact_dependency_graph in factory_app/workflows/extended_orchestration/extension_registry.json and loaded into GlobalPackGraph at runtime. It is the single source of truth for downstream propagation.
experience_spec is the first-class experience intent family produced by DesignDocs. It remains separate from the broader design_docs family so a page, navigation, or shell-experience refinement can invalidate experience intent before app-bundle regeneration without necessarily treating all design documentation as stale.
When a Family Becomes Stale¶
Every ArtifactVersionDoc carries a lifecycle_status:
| Status | Meaning |
|---|---|
draft | Generated, not yet reviewed |
current | Accepted and active |
stale | Invalidated by a later change; superseded in intent |
superseded | Replaced by a newer current version |
archived | Rejected or explicitly retired |
deleted | Hard-removed |
After a change request is accepted and routed, the ArtifactInvalidationService does two things:
-
Direct invalidation — marks the specific artifact version IDs in the session's tracked lineage as
staleusing thechange_request_idas the reason. This covers the families explicitly written by the selected workflow sequence (declared inaffected_declarative_familieson that sequence). -
Downstream propagation — performs a BFS traversal over
artifact_dependency_graph, finds all families that transitively depend on the written families, and marks all their non-archived/non-deleted versionsstaleviainvalidate_artifact_family(). This does not require the classifier or the session to know those families' specific version IDs.
Example: a concept_patch writes only concept. The invalidation service computes its downstream set (brand, design_docs, experience_spec, workflow_bundle, app_bundle) and marks all of them stale. The next refinement request will see all of them as needing resolution.
When Staleness Clears¶
A family is only considered stale if it has at least one stale version and no current version. Once a workflow run writes a new accepted current version for that family, the family drops off the stale list automatically — no explicit cleanup step is required.
This means the signal is always accurate: a family that was stale but then rebuilt correctly will not show as stale on the next request.
Two-Tier Staleness Routing¶
Staleness is handled in two tiers, applied in order:
Tier 1 — Deterministic Pre-Check (RefinementTriggerRouteResolver)¶
Before the LLM classifier is invoked, RefinementTriggerRouteResolver._stale_route() calls get_stale_artifact_families() for the request's app_id. If stale families are found, the router returns a deterministic decision immediately — the LLM is not called at all.
Priority order (earliest dependency wins):
| Priority | Stale family | Sequence used | Entry workflow |
|---|---|---|---|
| 1 | concept | full_rebuild | ValueEngine |
| 2 | brand | theme_revision | ThemeCapture |
| 3 | design_docs | design_revision | DesignDocs |
| 4 | experience_spec | app_surface_revision | DesignDocs |
| 5 | workflow_bundle | workflow_revision | AgentGenerator |
| 6 | app_bundle | app_revision | AppGenerator |
The ChangeIntent produced by the pre-check has source="stale_upstream" and confidence=1.0. The signals list carries the full set of detected stale families so the context seed is accurate for the restart workflow.
Tier 2 — LLM Classifier Advisory (LLMChangeClassifier)¶
When no stale families are detected, the flow proceeds to the LLM classifier at the request_submitted checkpoint. The classifier has access to the get_stale_artifact_families tool, which returns:
The classifier uses the result as an advisory signal alongside the user's request text to produce a patch | design | feature | core classification. The LLM tier only runs when Tier 1 finds nothing to act on.
Where the Dependency Graph Lives¶
"artifact_dependency_graph": {
"concept": [],
"brand": ["concept"],
"design_docs": ["concept"],
"experience_spec": ["concept", "design_docs"],
"workflow_bundle": ["design_docs"],
"app_bundle": ["design_docs", "experience_spec", "workflow_bundle", "brand"]
}
This field is part of GlobalPackGraph (mozaiksai/core/workflow/pack/schema.py). It must stay in sync with the actual workflow sequence ownership declared in affected_declarative_families on each sequence.
Artifact Persistence Per Workflow¶
Every workflow that produces a canonical artifact family must call persist_summary_artifact() so that ArtifactVersionDoc records exist in the DB with accurate lifecycle status. The stale-routing chain depends on these records being present.
| Workflow | Artifact family | Persistence call site |
|---|---|---|
| ValueEngine | concept | factory_app/workflows/ValueEngine/tools/manifest.py |
| ThemeCapture | brand | factory_app/workflows/ThemeCapture/tools/save_captured_theme.py |
| DesignDocs | design_docs | factory_app/workflows/DesignDocs/tools/save_design_doc.py |
| AgentGenerator | workflow_bundle | factory_app/workflows/AgentGenerator/tools/platform/build_lifecycle.py — emit_build_completed override |
| AppGenerator | app_bundle | factory_app/workflows/AppGenerator/tools/platform/build_lifecycle.py — emit_build_completed override |
AppGenerator and AgentGenerator persist their artifacts via the trigger: on_complete lifecycle hook, which fires after the workflow run finishes. The local build_lifecycle.py in each workflow overrides emit_build_completed from the shared module to call persist_summary_artifact() after the platform notification, reading build_mode from the persisted chat session context to correctly set revision_mode.
Artifact Version Refs — How Session State Stays Current¶
ArtifactInvalidationService.invalidate_for_change_request() reads SessionState.artifact_version_refs to mark specific version IDs stale (direct invalidation). Those refs must be accurate for every refinement request, including the very first one after an initial build.
After each workflow run completes, SessionRouter.advance_journey_after_run_complete() calls ArtifactStore.get_current_artifact_version_refs() to fetch all CURRENT artifact version IDs for the app, and merges them into state.artifact_version_refs before upserting. This means:
- All artifact-producing workflows (ValueEngine, ThemeCapture, DesignDocs, AgentGenerator, AppGenerator) populate the refs automatically at completion — no tool-level bookkeeping required.
- Revision routing via
_apply_revision_state()still overlays the specific requested artifact version ID, which takes precedence over the synced value. - The sync is best-effort: a failure is logged at DEBUG level and skipped.
Canonical Inputs — CURRENT-only Filter¶
resolve_latest_artifact_version_refs() (called inside persist_summary_artifact()) only resolves CURRENT artifact versions as canonical inputs. DRAFT versions created during an in-flight revision are never included.
- On first run, when no CURRENT version exists for a requested kind, that kind is simply absent from the returned dict. A DEBUG log line is emitted.
- DRAFT versions that have not yet been promoted to CURRENT do not appear in any downstream artifact's
canonical_inputs_version, so spurious staleness comparisons against in-flight draft IDs cannot occur. - The parent-version lookup in
persist_summary_artifact()also filters by CURRENT, ensuring revision DRAFTs always link to the last accepted CURRENT rather than another DRAFT.
Key Code Locations¶
| Concern | File |
|---|---|
| Dependency graph schema | mozaiksai/core/workflow/pack/schema.py — GlobalPackGraph.artifact_dependency_graph |
| Dependency graph data | factory_app/workflows/extended_orchestration/extension_registry.json |
| BFS propagation + direct invalidation | mozaiksai/control_plane/invalidation.py — ArtifactInvalidationService |
| Stale families query | mozaiksai/core/artifacts/store.py — get_stale_artifact_families() |
| Current version refs query | mozaiksai/core/artifacts/store.py — get_current_artifact_version_refs() |
Lifecycle-status filter on list_artifact_versions | mozaiksai/core/artifacts/store.py — lifecycle_status parameter |
| Canonical input resolution (CURRENT-only) | mozaiksai/core/artifacts/summary_artifacts.py — resolve_latest_artifact_version_refs() |
| Version refs sync after run | mozaiksai/core/session/router.py — advance_journey_after_run_complete() |
| Deterministic pre-check (Tier 1) | mozaiksai/control_plane/implementations/refinement_router.py — _stale_route() |
| Stale priority + sequence map | mozaiksai/control_plane/implementations/refinement_router.py — _STALE_PRIORITY, _STALE_SEQUENCE_MAP |
| Control plane tool (Tier 2) | factory_app/control_plane/tools/get_stale_artifact_families.py |
| Tool registration | factory_app/control_plane/config/tools.yaml |
| Classifier prompt rules | factory_app/control_plane/prompts/change_classifier_system.yaml |
| Lifecycle status enum | mozaiksai/core/artifacts/models.py — ArtifactLifecycleStatus |
Contributor Rules¶
affected_declarative_familieson each workflow sequence must be accurate. It drives both direct invalidation and the BFS starting set.- When adding a new artifact family, update
artifact_dependency_graphinextension_registry.json, add the family to_STALE_PRIORITYand_STALE_SEQUENCE_MAPinrefinement_router.py, and updateGlobalPackGraphinschema.pyif needed. - Do not add staleness routing logic to
control_plane.yaml. The deterministic pre-check inrefinement_router.pyandArtifactInvalidationServiceown that responsibility. - Do not add staleness logic to tools. Tools return data; the router and LLM reason.
- Tests for BFS propagation:
tests/test_control_plane_invalidation.py - Tests for staleness query:
tests/test_artifact_store.py - Tests for deterministic stale routing:
tests/test_refinement_router.py