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
@vibecodeqa/schema is described as the shared report contract — "a tiny leaf dependency" (README.md) that cli, app, mcp, action and github-app all build on. In practice the report contract now lives in cli/src/, and this package publishes a subset of it that no longer describes what the CLI writes.
Nobody notices, because parseReport is deliberately forward-compatible: every object is .passthrough() and CheckResult.details is z.record(z.unknown()) (src/report-schema.ts:22-28). Undocumented fields sail through validation and land in consumers as unknown. The cost shows up as casts and hand-copied interfaces in three repos.
Evidence
1. ToolRun — published shape is 7 fields, the CLI writes 13
// cli/src/runners/exec.ts:14-33 — the interface that actually produces the dataexportinterfaceToolRun{tool: string;command: string;cwd: string;analyzerId?: string;analyzer?: string;projectId?: string;projectPath?: string;status: "success"|"failed";exitCode: number|null;ok: boolean;durationMs: number;output: string;notFound: boolean;}
Six fields the schema does not have: analyzerId, analyzer, projectId, projectPath, status, exitCode. They arrived with vibecodeqa/cli#59 (closed 2026-08-07, "Package-tagged tool provenance"); the schema's ToolRun has not changed since 0.4.0 (01c757e, 2026-07-24).
Two structural details make this worse than a normal lag:
The CLI never imports the schema's ToolRun. It declares its own at cli/src/runners/exec.ts:14. So the published type is not a lagging copy that a build would catch — it is an unreferenced parallel declaration. There is no mechanism by which it could ever fail.
The schema's ToolRun is orphaned inside this package.ToolRun appears exactly once in src/, at its own declaration; no other type references it and report-schema.ts has no ToolRunSchema. parseReport() therefore never validates a tool run at all.
The consumer pays for it directly. The CLI attaches runs at cli/src/core.ts:264-266 (result.details = { ...result.details, toolRuns }), and because CheckResult has no toolRuns field, the app reads it through a cast:
schema/src/types.ts:86-92 has neither. ProjectContext (cli/src/types.ts:50-69), ProjectDiscoveryEvidence, ProjectToolCommand and ProjectSupport exist only in the CLI.
3. Types that are hand-copied into a third repo
AnalyzerMetric / AnalyzerSnapshot are declared in cli/src/types.ts:13-30, emitted at report.meta.analyzerSnapshots[], absent from this package — and re-declared verbatim in the app:
// app/src/monitor/views/analyzerMetrics.logic.ts:34-50, under a header that says// "The types the app consumes (mirroring the CLI contract exactly)."exportinterfaceAnalyzerSnapshot{analyzerId: string;status: AnalyzerStatus;score?: number; ... }
Two hand-maintained copies of one contract, in repos that release independently.
4. Fields the CLI writes into every check, undeclared here
cli/src/core.ts:340 stamps status, scoreMode and scoreImpact onto every check's details, using CheckStatus and ScoreMode unions declared at cli/src/core.ts:72-75. Neither union exists in this package, and CheckResult (src/types.ts:26-33) has no status.
The CLI is already reaching for a schema field that was never declared:
5. There is uncommitted schema work sitting in a stash
git stash list in this repo shows stash@{0}: On main: WIP app#27/schema half: ProjectContext + AnalyzerSnapshot schemas (backed up /tmp/vcqa-wip-backup/) [stashed by dev-agent 2026-08-08] — 149 insertions across src/types.ts, src/report-schema.ts, src/check-meta.ts, test/schema.test.ts. That work is not on main and not published. It should be recovered or deliberately discarded as part of whatever this issue decides; leaving it stashed is the worst of the three.
The decision
Is @vibecodeqa/schema the source of truth for the report contract, or a published mirror of it? Today it is neither on purpose, which is why it drifts.
Option A — schema owns the contract (recommended)
Rule: any type that appears in report.json is declared here first; cli and app import it and may not re-declare it. A CLI change that adds a report field is blocked on a schema release.
Ends the parallel declarations by making them impossible — the CLI's exec.ts would import ToolRun and a missing field becomes a type error at build time, not a silent divergence.
Matches what cli/CLAUDE.md:154 and :172 already assert for CHECK_META ("the schema package is the source of truth"). Extends an existing, accepted rule rather than inventing one.
Cost: two-step releases. Adding a report field means schema release → consumer bump → CLI release. That is real friction on a repo that releases via CI-only OIDC publishing.
Mitigation for the friction: batch. Nothing here needs to be per-field.
Option B — CLI owns the contract, schema is a generated/checked mirror
Keep declaring types in cli/src/, and add a CI check in this repo (or the CLI's) that fails when the published schema and the CLI's types diverge — e.g. a fixture-based conformance test in the spirit of mcp/test/report-compat.test.mjs.
Zero friction for CLI development; drift becomes loud instead of silent.
Cost: this package stops being a contract and becomes documentation. Consumers still get types late, and app still has to choose between waiting and copying. Does not fix the app-side copy of AnalyzerSnapshot.
Option C — status quo
Local extension in the CLI, schema bumped ad hoc when someone notices.
The four releases since 0.4.0 are the evidence for what this produces: an orphaned ToolRun, a stashed branch of unlanded work, a hand-copied AnalyzerSnapshot, and two open consumer issues asking for the same interface.
Recommendation
Option A for anything that crosses a package boundary in report.json (ToolRun, CheckResult, WorkspaceInfo, ProjectContext, AnalyzerSnapshot, StackInfo, the CheckStatus/ScoreMode unions), Option C explicitly for CLI-internal types (ToolRunContext, ToolRunFilter, NormalizedCheckResult — these never appear in a report and should not be published). Write that boundary into this repo's README so the next contributor does not have to infer it.
Then land the backlog as one 0.5.0, not three:
ToolRun gains the six fields the CLI already writes (analyzerId, analyzer, projectId, projectPath, status, exitCode).
ToolRun gains an origin/source discriminator — vibecodeqa/app#52.
CheckResult gains an optional toolRuns?: ToolRun[], or the contract states in prose that details.toolRuns is the canonical location (see open question).
WorkspaceInfo.projects / .discovery, ProjectContext, AnalyzerSnapshot, AnalyzerMetric move here — starting from stash@{0}, which already drafted most of this.
A ToolRunSchema in report-schema.ts so parseReport actually validates a tool run.
Landing app#52 and cli#26 separately means two schema releases and two rounds of consumer bumps for edits to the same interface.
Consequence of not deciding
app#52 is labelled blocked in a repo that cannot unblock it. cli#26's acceptance criteria include emitting truncated and stderr, which the CLI can do unilaterally — and it will, into an undocumented details blob, exactly as analyzerId and exitCode went in. Each iteration adds another cast in app and another mirrored interface. mcp continues to pin a schema that validates a report shape which does not describe the fields its own tools read.
Acceptance criteria
This repo's README states which types are owned here and which are CLI-internal, in one short section.
ToolRun in src/types.ts is field-for-field a superset of what cli/src/runners/exec.ts writes, and cli/src/runners/exec.ts imports it instead of declaring its own (CLI-side follow-up, but the schema half must land first).
parseReport() validates toolRuns entries rather than passing them through as unknown.
app/src/monitor/views/analyzerMetrics.logic.ts can delete its mirrored AnalyzerMetric/AnalyzerSnapshot declarations and import them (app-side follow-up).
stash@{0} is either landed or dropped, and the issue says which.
One release covers app#52 and cli#26, not two.
Constraints the implementer would otherwise miss
Releases are CI-only via npm OIDC trusted publishing and require node 24 / npm >= 11. Never npm publish locally.
Every consumer pins independently — mcp 0.4.2, app^0.4.1, cli^0.4.2. A schema release changes nothing for a user until each consumer bumps and releases. Sequence the bumps.
.passthrough() everywhere is a deliberate design decision (README.md, "parseReport() keeps the report shape strict where the vocabulary is truly closed... and forward-compatible where producers evolve"). Adding fields must not turn into tightening validation on old reports — mcp parses a committed fixture from a pinned older CLI (mcp/test/report-compat.test.mjs) and that must keep passing.
cli/src/core.ts:75 already defines ScoreMode; if it is published here, the union must match exactly or the cast at cli/src/core.ts:377 cannot be removed cleanly.
Canonical location for tool-run provenance. app#52 says the "contract's top-level check.toolRuns" is empty while analyzers write details.toolRuns. Verified: there is no top-level toolRuns — CheckResult in src/types.ts:26-33 has no such field and cli/src/core.ts:264-266 only ever writes details.toolRuns. So the choice is (a) promote it to a declared CheckResult.toolRuns? and migrate producers, or (b) declare details.toolRuns canonical and document it. (b) is cheaper and breaks nothing; (a) is cleaner and is a breaking change for every consumer reading the nested path. This is a maintainer call, not an implementer one.
analyzer vs analyzerId.cli/src/runners/exec.ts:21-22 carries both, with cli/src/runners/exec.ts:87 normalising analyzerId ?? analyzer. Publish both, or publish only analyzerId and treat analyzer as a deprecated CLI-internal alias?
Recommendation on vibecodeqa/app#52
Move it here. It asks for a field on a type this package owns, it is labelled blocked in a repo that has no ability to unblock it, and it is currently the only tracking anywhere for that schema change. Suggested handling:
gh issue transfer 52 vibecodeqa/schema, then rescope its body to the schema half (the ToolRun origin/source discriminator), and fold its open provenance-location question into open question 1 above.
File a small app-side follow-up for the part that genuinely belongs to app: ToolLogModal's empty state hardcodes "These findings come from the built-in analyzer", which becomes false the moment an external analyzer runs. That is a copy change in app, independent of the schema.
Leaving it in app is the alternative, and the reason to reject it is that it hides a schema dependency from the repo that owns it — this repo had zero issues while two other repos tracked work that only it can do.
Related
vibecodeqa/app#52 — ToolRun origin field (recommend transferring here, above).
Problem
@vibecodeqa/schemais described as the shared report contract — "a tiny leaf dependency" (README.md) thatcli,app,mcp,actionandgithub-appall build on. In practice the report contract now lives incli/src/, and this package publishes a subset of it that no longer describes what the CLI writes.Nobody notices, because
parseReportis deliberately forward-compatible: every object is.passthrough()andCheckResult.detailsisz.record(z.unknown())(src/report-schema.ts:22-28). Undocumented fields sail through validation and land in consumers asunknown. The cost shows up as casts and hand-copied interfaces in three repos.Evidence
1.
ToolRun— published shape is 7 fields, the CLI writes 13Six fields the schema does not have:
analyzerId,analyzer,projectId,projectPath,status,exitCode. They arrived with vibecodeqa/cli#59 (closed 2026-08-07, "Package-tagged tool provenance"); the schema'sToolRunhas not changed since 0.4.0 (01c757e, 2026-07-24).Two structural details make this worse than a normal lag:
ToolRun. It declares its own atcli/src/runners/exec.ts:14. So the published type is not a lagging copy that a build would catch — it is an unreferenced parallel declaration. There is no mechanism by which it could ever fail.ToolRunis orphaned inside this package.ToolRunappears exactly once insrc/, at its own declaration; no other type references it andreport-schema.tshas noToolRunSchema.parseReport()therefore never validates a tool run at all.The consumer pays for it directly. The CLI attaches runs at
cli/src/core.ts:264-266(result.details = { ...result.details, toolRuns }), and becauseCheckResulthas notoolRunsfield, the app reads it through a cast:2.
WorkspaceInfo— extended locally in the CLIschema/src/types.ts:86-92has neither.ProjectContext(cli/src/types.ts:50-69),ProjectDiscoveryEvidence,ProjectToolCommandandProjectSupportexist only in the CLI.3. Types that are hand-copied into a third repo
AnalyzerMetric/AnalyzerSnapshotare declared incli/src/types.ts:13-30, emitted atreport.meta.analyzerSnapshots[], absent from this package — and re-declared verbatim in the app:Two hand-maintained copies of one contract, in repos that release independently.
4. Fields the CLI writes into every check, undeclared here
cli/src/core.ts:340stampsstatus,scoreModeandscoreImpactonto every check'sdetails, usingCheckStatusandScoreModeunions declared atcli/src/core.ts:72-75. Neither union exists in this package, andCheckResult(src/types.ts:26-33) has nostatus.The CLI is already reaching for a schema field that was never declared:
5. There is uncommitted schema work sitting in a stash
git stash listin this repo showsstash@{0}: On main: WIP app#27/schema half: ProjectContext + AnalyzerSnapshot schemas (backed up /tmp/vcqa-wip-backup/) [stashed by dev-agent 2026-08-08]— 149 insertions acrosssrc/types.ts,src/report-schema.ts,src/check-meta.ts,test/schema.test.ts. That work is not onmainand not published. It should be recovered or deliberately discarded as part of whatever this issue decides; leaving it stashed is the worst of the three.The decision
Is
@vibecodeqa/schemathe source of truth for the report contract, or a published mirror of it? Today it is neither on purpose, which is why it drifts.Option A — schema owns the contract (recommended)
Rule: any type that appears in
report.jsonis declared here first;cliandappimport it and may not re-declare it. A CLI change that adds a report field is blocked on a schema release.exec.tswould importToolRunand a missing field becomes a type error at build time, not a silent divergence.cli/CLAUDE.md:154and:172already assert forCHECK_META("the schema package is the source of truth"). Extends an existing, accepted rule rather than inventing one.Option B — CLI owns the contract, schema is a generated/checked mirror
Keep declaring types in
cli/src/, and add a CI check in this repo (or the CLI's) that fails when the published schema and the CLI's types diverge — e.g. a fixture-based conformance test in the spirit ofmcp/test/report-compat.test.mjs.appstill has to choose between waiting and copying. Does not fix the app-side copy ofAnalyzerSnapshot.Option C — status quo
Local extension in the CLI, schema bumped ad hoc when someone notices.
ToolRun, a stashed branch of unlanded work, a hand-copiedAnalyzerSnapshot, and two open consumer issues asking for the same interface.Recommendation
Option A for anything that crosses a package boundary in
report.json(ToolRun,CheckResult,WorkspaceInfo,ProjectContext,AnalyzerSnapshot,StackInfo, theCheckStatus/ScoreModeunions), Option C explicitly for CLI-internal types (ToolRunContext,ToolRunFilter,NormalizedCheckResult— these never appear in a report and should not be published). Write that boundary into this repo's README so the next contributor does not have to infer it.Then land the backlog as one 0.5.0, not three:
ToolRungains the six fields the CLI already writes (analyzerId,analyzer,projectId,projectPath,status,exitCode).ToolRungains an origin/source discriminator — vibecodeqa/app#52.ToolRungainstruncated,stderr,startedAt— Capture the complete, verifiable log for EVERY delegated tool (trust: prove knip/eslint/etc. actually ran) cli#26.CheckResultgains an optionaltoolRuns?: ToolRun[], or the contract states in prose thatdetails.toolRunsis the canonical location (see open question).WorkspaceInfo.projects/.discovery,ProjectContext,AnalyzerSnapshot,AnalyzerMetricmove here — starting fromstash@{0}, which already drafted most of this.ToolRunSchemainreport-schema.tssoparseReportactually validates a tool run.Landing app#52 and cli#26 separately means two schema releases and two rounds of consumer bumps for edits to the same interface.
Consequence of not deciding
app#52 is labelled
blockedin a repo that cannot unblock it. cli#26's acceptance criteria include emittingtruncatedandstderr, which the CLI can do unilaterally — and it will, into an undocumenteddetailsblob, exactly asanalyzerIdandexitCodewent in. Each iteration adds another cast inappand another mirrored interface.mcpcontinues to pin a schema that validates a report shape which does not describe the fields its own tools read.Acceptance criteria
ToolRuninsrc/types.tsis field-for-field a superset of whatcli/src/runners/exec.tswrites, andcli/src/runners/exec.tsimports it instead of declaring its own (CLI-side follow-up, but the schema half must land first).parseReport()validatestoolRunsentries rather than passing them through asunknown.app/src/monitor/views/analyzerMetrics.logic.tscan delete its mirroredAnalyzerMetric/AnalyzerSnapshotdeclarations and import them (app-side follow-up).stash@{0}is either landed or dropped, and the issue says which.Constraints the implementer would otherwise miss
npm publishlocally.mcp0.4.2,app^0.4.1,cli^0.4.2. A schema release changes nothing for a user until each consumer bumps and releases. Sequence the bumps..passthrough()everywhere is a deliberate design decision (README.md, "parseReport() keeps the report shape strict where the vocabulary is truly closed... and forward-compatible where producers evolve"). Adding fields must not turn into tightening validation on old reports —mcpparses a committed fixture from a pinned older CLI (mcp/test/report-compat.test.mjs) and that must keep passing.cli/src/core.ts:75already definesScoreMode; if it is published here, the union must match exactly or the cast atcli/src/core.ts:377cannot be removed cleanly.dead-codemetadata), which also proposes aCheckMeta.scoreMode. If both land, land them in the same release.Open questions for the maintainer
check.toolRuns" is empty while analyzers writedetails.toolRuns. Verified: there is no top-leveltoolRuns—CheckResultinsrc/types.ts:26-33has no such field andcli/src/core.ts:264-266only ever writesdetails.toolRuns. So the choice is (a) promote it to a declaredCheckResult.toolRuns?and migrate producers, or (b) declaredetails.toolRunscanonical and document it. (b) is cheaper and breaks nothing; (a) is cleaner and is a breaking change for every consumer reading the nested path. This is a maintainer call, not an implementer one.analyzervsanalyzerId.cli/src/runners/exec.ts:21-22carries both, withcli/src/runners/exec.ts:87normalisinganalyzerId ?? analyzer. Publish both, or publish onlyanalyzerIdand treatanalyzeras a deprecated CLI-internal alias?Recommendation on vibecodeqa/app#52
Move it here. It asks for a field on a type this package owns, it is labelled
blockedin a repo that has no ability to unblock it, and it is currently the only tracking anywhere for that schema change. Suggested handling:gh issue transfer 52 vibecodeqa/schema, then rescope its body to the schema half (theToolRunorigin/source discriminator), and fold its open provenance-location question into open question 1 above.app:ToolLogModal's empty state hardcodes "These findings come from the built-in analyzer", which becomes false the moment an external analyzer runs. That is a copy change inapp, independent of the schema.Leaving it in
appis the alternative, and the reason to reject it is that it hides a schema dependency from the repo that owns it — this repo had zero issues while two other repos tracked work that only it can do.Related
ToolRunorigin field (recommend transferring here, above).truncated/stderr/startedAtonToolRun.AnalyzerSnapshotshape that never reached this package.detailsfields described above.cli#20(extract this package) as done.dead-codemetadata gap; theCheckMetahalf of the same drift.