You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Factory status-adapter.ts validates run-index.json (v2) and status.json (v1) using ~177 lines of hand-rolled type-guard functions (isRecord, isOptionalString, isValidPhaseStatus, etc.). While this works and has comprehensive test coverage, it has several drawbacks:
No reusable schema definition — the shape of run-index.json is documented only implicitly by TypeScript interfaces and validation functions scattered across status-adapter.ts
No standalone validation tool — validating run output requires running the full Express server; there is no CLI script to validate files on disk
Maintenance burden — every new field requires updating both a TypeScript interface and a corresponding type-guard function, with no compile-time guarantee they stay in sync
No structured error reporting — validation throws a generic "Invalid run-index.json at {path}" with no detail on which field failed
This ticket introduces a schema-validation library, defines schemas for the run-index.json data structure, creates a standalone validation script, and refactors the status-adapter to use the schemas.
Schemas live in server/adapters/schemas/ — V2RunIndex and V1StatusObject describe on-disk formats read only by the server. Client code never touches raw formats.
canonical.ts types stay hand-written — CanonicalRunStatus is a flat domain model; V2RunIndex is nested (context + config). The normalizeV2/normalizeV1 functions map between these shapes. Replacing canonical types with z.infer<> would require complex transforms and touch 15+ importing files. Additionally, exactOptionalPropertyTypes is enabled — ArtifactEntry uses iteration?: number (property can be absent), which differs from Zod's .optional() producing T | undefined (must be present).
Phase entries use .passthrough() for forward-compatibility — The current validation allows unknown keys in phase objects and only validates known ones. A z.object({...}).passthrough() preserves this.
Zod is a runtime dependency — Schemas are used at runtime in the server adapter, not just at build time. However, since schemas are in server/adapters/, they are not included in the client bundle (Vite tree-shakes them).
Acceptance criteria
Must have
Add zod as a runtime dependency in packages/factory/ with exact version pinning
Description
The Factory
status-adapter.tsvalidatesrun-index.json(v2) andstatus.json(v1) using ~177 lines of hand-rolled type-guard functions (isRecord,isOptionalString,isValidPhaseStatus, etc.). While this works and has comprehensive test coverage, it has several drawbacks:run-index.jsonis documented only implicitly by TypeScript interfaces and validation functions scattered acrossstatus-adapter.ts"Invalid run-index.json at {path}"with no detail on which field failedThis ticket introduces a schema-validation library, defines schemas for the run-index.json data structure, creates a standalone validation script, and refactors the status-adapter to use the schemas.
Proposed library: Zod
Zod is the proposed library:
z.infer<>, eliminating duplication between interfaces and validation functionsZodErrorprovides structured, per-field error paths, messages, and codesAlternatives considered
If the team decides against Zod, Valibot is the closest alternative.
Architecture context
Data flow
Key files
packages/factory/src/server/adapters/status-adapter.tspackages/factory/src/server/adapters/__tests__/status-adapter.test.tspackages/factory/src/shared/types/canonical.tsCanonicalRunStatus,Phases,ArtifactEntry, etc. (18 exported types)packages/factory/src/server/services/project-scanner.tsparseRunData()packages/factory/src/__test-helpers__/fixtures.tscreateMockRunStatus(),emptyPhases(),createCompletedRunPhases()V2 run-index.json structure
{ "version": 2, "context": { "runId": "string", // required "projectSlug": "string", // required "ticketId": "string?", // optional "projectRoot": "string", // required "branch": "string", // required "task": "string", // required "startedAt": "string", // required (ISO datetime) "completedAt": "string?", // optional, nullable "status": "RunStatus", // enum: in_progress | completed | failed | needs_manual_review "phases": { // each value is nullable (null = not yet reached) "architecture": { "status": "PhaseStatus", "impactLevel?": "string", "artifact?": "string" }, "planning": { "status": "PhaseStatus", "stepCount?": "number", "artifacts?": ["string"] }, "implementation": { "status": "PhaseStatus", "artifact?": "string", "qualityGates?": "..." }, "parallelReview": { "aggregatedCriticality": "Criticality", "reviewRoundsUsed": "number", "..." }, "review": { "status": "PhaseStatus", "iterations?": "number", "finalCriticality?": "Criticality" }, "codeSimplifier": { "ran": "boolean", "actionableFindings": "boolean", "..." }, "holisticReview": { "status": "PhaseStatus", "criticality?": "Criticality", "..." } }, "phaseDecisions?": { "[phase]": { "run": "boolean", "reason?": "string" } } }, "config": { "externalPlan?": "boolean", "mergeBaseSha?": "string", "diffBase?": "string", "maxReviewRounds?": "number", "fixLowFindings?": "boolean", "mode?": "string", "model?": "string" }, "artifacts?": [ { "filename": "string", // required "role": "string", // required "roleType": "string", // required "agent": "string", // required "type": "string", // required "phase": "string", // required "createdAt": "string", // required "iteration?": "number", // optional "note?": "string" // optional } ] }Enum values
RunStatus:in_progress,completed,failed,needs_manual_reviewPhaseStatus:completed,skipped,failed,in_progress,approvedCriticality:none,low,medium,highReviewerStatus:completed,skipped,failedValidation behaviors to preserve
These behaviors are verified by the existing 900-line test suite:
nullphase entries are valid (phase not yet reached)completedAtacceptsnull,undefined, orstringphasesobject may contain unknown phase keys (forward-compatible){}phase entries are acceptedphaseDecisionsentries requirerun: boolean;reasonis optionalstatus,criticality,finalCriticality,aggregatedCriticalityacceptnullor their respective enum valuesiterationandnoteare optionalRun-index.json file locations
Run-index.json files are produced by the orchestrated-development pipeline and stored at:
Key design decisions
Schemas live in
server/adapters/schemas/— V2RunIndex and V1StatusObject describe on-disk formats read only by the server. Client code never touches raw formats.canonical.tstypes stay hand-written —CanonicalRunStatusis a flat domain model; V2RunIndex is nested (context+config). ThenormalizeV2/normalizeV1functions map between these shapes. Replacing canonical types withz.infer<>would require complex transforms and touch 15+ importing files. Additionally,exactOptionalPropertyTypesis enabled —ArtifactEntryusesiteration?: number(property can be absent), which differs from Zod's.optional()producingT | undefined(must be present).Phase entries use
.passthrough()for forward-compatibility — The current validation allows unknown keys in phase objects and only validates known ones. Az.object({...}).passthrough()preserves this.Zod is a runtime dependency — Schemas are used at runtime in the server adapter, not just at build time. However, since schemas are in
server/adapters/, they are not included in the client bundle (Vite tree-shakes them).Acceptance criteria
Must have
zodas a runtime dependency inpackages/factory/with exact version pinningstatus-adapter.tsto replace hand-rolled type guards with Zod.safeParse()— preserve public API and error messagespnpm run validate:run-indexin factorypackage.jsonShould have
Nice to have
project-scanner.tsto surface structured validation errors instead of silently skipping invalid runs