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
Decision required before any #28 code is written. This issue asks for one thing: the final member list of AnalyzerContext, in particular whether files: FileInventory and policy: EffectiveScanPolicy are members. Everything below exists to make that decision answerable in one sitting.
docs/internal-analyzer-contract.md is the implementation spec #28 names. Its AnalyzerContext sketch predates two things that have since shipped in 0.55.0, and it has not been amended for either:
The words FileInventory, EffectiveScanPolicy and inventorydo not appear anywhere in the document — verified by grep across all 338 lines. The only ignore-related member is ignoreNames: string[] (line 93), which is the pre-#71 representation: a flat list of names, with no reason codes, no precedence, and no notion of the security-sensitive override that #71 added. In main that list is now a derived view, not a source of truth — src/core.ts:125:
The same sketch is duplicated in the body of #28 itself, in an even older form (no projects / project members at all). Both copies need the same amendment.
app/CLAUDE.md:100 makes this a precondition rather than a follow-up:
If these docs disagree, fix the docs before implementing the feature.
The doc and main disagree today. So the ordering is: settle this issue, amend docs/internal-analyzer-contract.mdand the sketch in #28's body, then write #28 code.
The gap is already recorded in the code's own history
This is not a hypothetical. The closing comment on #70 says so explicitly, under "Scoped out, deliberately":
I implemented the equivalent as module-level queries — inventorySourceFiles, inventoryTestFiles, inventoryAllFiles, inventoryFiles({ kind, ext, includeGenerated, includeIgnored }) — rather than a ctx.files object, because there is no AnalyzerContext object to hang it off yet.
And #71's closing comment describes the policy surface as landing where the context would have:
FileInventory now carries the EffectiveScanPolicy it was built from, plus inventoryIsIgnored / inventoryClassify / inventoryExplain / inventoryHas — the ctx.isIgnored(path) / ctx.classify(path) surface from #70's sketch.
That carrying is real, src/file-inventory.ts:54-58:
exportinterfaceFileInventory{root: string;/** The one EffectiveScanPolicy this inventory was built from. Carried here so * every runner that receives the inventory receives the same policy, and can * classify or explain a path the walk never produced (#71). */policy: EffectiveScanPolicy;
Meanwhile the de-facto context in main is a widening positional argument list — src/core.ts:136-156 passes (resolvedCwd, stack, workspace, fileInventory), (resolvedCwd, workspace, fileInventory), and for testing a six-argument call (resolvedCwd, stack, skipTests, srcRoots, workspace, fileInventory). Every runner takes the inventory as an optional trailing parameter (src/runners/secrets.ts:255, src/runners/flutter.ts:36, src/runners/structure.ts:34). Replacing that with a typed context is exactly what #28 is for, which is why the member list has to be right before it is frozen into 30+ call sites.
The decision
D1 — Does AnalyzerContext carry the file universe? (primary)
Recommendation: yes — add files: FileInventory and delete ignoreNames.
Option
What it means
Cost
A. files: FileInventory (recommended)
The context carries the built inventory object. Runners keep calling the existing module-level queries, passing ctx.files.
One member. Zero change to file-inventory.ts. Matches what core.ts already passes positionally.
B. Accessor functions on the context
ctx.sourceFiles(), ctx.testFiles(), ctx.allFiles(opts) — closures bound to the inventory, inventory itself not exposed.
Nicer call sites, but it is a second API over the one shipped in #70, and it hides ctx.files.policy, which #71 deliberately put on the inventory. Also cannot express includeIgnored cases like flutter's generated-Dart ratio without re-adding an escape hatch.
C. Leave it out; runners keep taking the inventory positionally
No decision needed now.
Defeats #28's purpose — core.ts keeps a bespoke argument list per runner, which is the thing the issue says it is removing.
Rejecting B specifically: it is not wrong, but it duplicates a surface that shipped four days ago and has tests (src/file-inventory.test.ts). If accessors are wanted later they can be added as sugar overctx.files without another migration. Rejecting C: it makes #28 a rename rather than a contract.
ignoreNames: string[] should be deleted, not kept alongside. It is a lossy projection of ctx.files.policy and, per #71's commit message, keeping two representations of one contract is what caused the fs-utils drift that #71 existed to remove. If a caller genuinely wants the flat list, policyIgnoreNames(ctx.files.policy) produces it.
D2 — Is policy a separate member, or reached via ctx.files.policy?
Recommendation: ctx.files.policy only. Do not add a top-level policy member.
The inventory already owns exactly one policy and documents why (src/file-inventory.ts:56-58, quoted above). A second reference creates a shape where ctx.policy !== ctx.files.policy is constructible, which is precisely the two-copies-of-one-contract failure mode.
Counter-argument worth recording: out-of-process analyzers (#32) have to serialise the context over JSON/stdio, and EffectiveScanPolicy is a small flat record (src/scan-policy.ts:43-54, nine fields) while FileInventory carries every walked path plus ignoredFiles. A subprocess protocol will likely send the policy and a filtered file list rather than the whole inventory. That is an argument about the wire shape, not the in-process shape, and #32 is free to define its own projection. It is not a reason to split the member here — but it should be written into the doc so #32's author does not read files: FileInventory as a serialisation requirement.
D3 — Do settings / effectiveSettings stay as sketched? (unblocks #30)
The sketch has both (docs/internal-analyzer-contract.md:89-90). Recommendation: keep both, and specify that settings is the raw user block and effectiveSettings is defaults-merged-and-validated, since the names do not say so.
There is prior thinking on this that is not in the repo — see "Uncommitted work" below. That WIP resolves settings once per scan in core.ts and delivers them via details.effectiveSettings / details.settingsWarnings per check plus meta.analyzerSettings on the report, with a new EffectiveAnalyzerSettings type. That approach needs no AnalyzerContext at all.
So D3 has a sequencing sub-question the maintainer should answer: does #30 land on today's positional runners (as the WIP does), or does it wait for #28's context? Landing first is cheaper and unblocks the app surface sooner; waiting avoids writing a settings path that #28 immediately reworks. I lean toward landing #30 first provided the report-level meta.analyzerSettings shape is treated as the stable contract and the plumbing is understood to be temporary — but this is genuinely the maintainer's call, not mine.
#58's first task is "Add analyzer scope metadata: repo, project, file-graph, derived". The doc already half-specifies this in prose — line 109, "Repo-level analyzers receive the whole projects list. Project-scoped analyzers receive one project at a time" — and lifecycle step 2, line 140, "Select repo or project scope from the analyzer manifest". But no scope field exists on AnalyzerManifest (docs/internal-analyzer-contract.md:66-74). The lifecycle instructs the engine to read a field the type does not have.
Recommendation: add scope: "repo" | "project" | "file-graph" | "derived" to AnalyzerManifest, and keep both projects? and project? on the context as the sketch already has them.ProjectContext is already a shipped type (src/types.ts:50-63), so this costs nothing new. Rejected alternative: inferring scope from whether the analyzer reads ctx.project — unknowable statically, and the engine has to decide iteration before calling the analyzer.
D5 — Does the context carry analyzer identity, for tool-run provenance? (relates to app#52)
app#52 wants ToolRun to record where a run came from, and proposes { analyzerId, source: "builtin" | "plugin", pluginId?, version? }. Lifecycle step 7 (line 145) already says "Attach package-tagged tool provenance from exec.ts", but nothing in the context tells exec.tswhich analyzer is running.
Recommendation: add analyzerId: string to AnalyzerContext so the engine can stamp origin at the recording boundary rather than asking every analyzer to self-report. This is a one-word change here that makes app#52 implementable without touching each runner. Note app#52 lives in vibecodeqa/app but the field it wants is in @vibecodeqa/schema's ToolRun — that part is not this issue's to decide, and this issue only commits to making the CLI side supplyable.
D6 — #58 vs #27: merge or parent? (sub-decision, as raised by the audit)
Verified: both are open and both are labelled analyzer-platform, and #58 already lists #27 under its Related section, so the overlap is acknowledged but not resolved.
Recommendation: explicit parenting, not merging. Keep #58 as the implementation issue and rewrite #27's body to be the acceptance checkpoint for it — "verified by" rather than "also does". Reason for not merging: #27 carries the observable symptom a user reported, and folding it into #58 loses a separately closable proof that the symptom is gone. Reason for not leaving them as-is: two open issues describing one body of work will get picked up twice.
If the maintainer prefers merging, close #27 as duplicate of #58after copying its symptom evidence into #58's acceptance criteria — not before.
What each choice unblocks
Decision
Unblocks
D1 files: FileInventory
#28 (a context worth migrating to); the ctx.files sketch #70 deferred; retires the residue of #70
D2 policy via ctx.files.policy
the residue of #71 ("every runner receives the same policy through analyzer context" — currently satisfied only by the inventory hop); informs #32
Known residues that are not resolved by this decision
Both are named in #70's closing comment and neither has an issue, so they should not be assumed in scope:
files.html({ deliverable: true }) and files.config({ kind: "github-actions" }) were sketched in Introduce FileInventory: classify scan files once and require runners to consume it #70 and have no implementation. The missing config query is why lint and best-practices still read .github/workflows/ directly. If the context is to expose them, that needs its own issue.
External tool adapters (lint, types) let the tool discover files and filter output afterwards through isIgnoredPath(). That is deliberate and documented in docs/exclusion-policy.md under "Walks that remain, and why". The context does not change it.
Uncommitted work that should be read before deciding, and rescued
There is a stash in the local cli checkout:
stash@{0}: On main: WIP cli#30 analyzer-settings schema (backed up /tmp/vcqa-wip-backup/) [stashed by dev-agent 2026-08-08]
It touches docs/internal-analyzer-contract.md (+11 lines, a "Current implementation" block under Settings), src/types.ts (adds EffectiveAnalyzerSettings and meta.analyzerSettings), src/core.ts (+64/-8), src/config.ts, and tests. It is the prior thinking behind D3 and contains a concrete answer to "where do effective settings surface".
It is backed up only to /tmp/vcqa-wip-backup/ (stash-0.patch, plus cli-uncommitted.patch, schema-uncommitted.patch, app-uncommitted-*.patch, cli-untracked.tgz). macOS prunes /tmp and the stash lives in one working copy on one machine; neither survives indefinitely. Whoever picks this up should move that backup somewhere durable, or land/drop the stash, before it evaporates.
Acceptance criteria
This issue records a decision, as a comment, on D1 and D2 at minimum — the explicit yes/no on files: FileInventory and on a separate policy member.
docs/internal-analyzer-contract.md's AnalyzerContext block matches the decision, and the document mentions FileInventory and EffectiveScanPolicy by name with a pointer to docs/exclusion-policy.md.
If ignoreNames is dropped, the doc says what replaces it (policyIgnoreNames(ctx.files.policy)).
app/CLAUDE.md:100 — "If these docs disagree, fix the docs before implementing the feature." The doc amendment is a precondition, not cleanup.
cli/CLAUDE.md — trunk-based, commit straight to main, no branches and no PRs. There is no PR to hold this decision in; the decision has to live in this issue.
cli/CLAUDE.md stack-gating rule still applies to whatever the context exposes: a stack.framework === ... branch inside a generic analyzer is a rejected diff regardless of how the context is shaped.
Doc-only pushes trigger publish.yml, which then skips publishing unless package.json version changed. Amending the doc is safe; it just starts a job that no-ops.
Decision required before any #28 code is written. This issue asks for one thing: the final member list of
AnalyzerContext, in particular whetherfiles: FileInventoryandpolicy: EffectiveScanPolicyare members. Everything below exists to make that decision answerable in one sitting.Why this blocks #28
docs/internal-analyzer-contract.mdis the implementation spec #28 names. ItsAnalyzerContextsketch predates two things that have since shipped in 0.55.0, and it has not been amended for either:FileInventory—9764ca3, "make FileInventory the file universe every runner uses" (Introduce FileInventory: classify scan files once and require runners to consume it #70, closed).EffectiveScanPolicy—85278f7, "make EffectiveScanPolicy the only ignore engine" (Define effective scan policy: ignore precedence, generated classification, and security overrides #71, closed).The sketch as it stands today,
docs/internal-analyzer-contract.md:82-94:The words
FileInventory,EffectiveScanPolicyandinventorydo not appear anywhere in the document — verified by grep across all 338 lines. The only ignore-related member isignoreNames: string[](line 93), which is the pre-#71 representation: a flat list of names, with no reason codes, no precedence, and no notion of the security-sensitive override that #71 added. Inmainthat list is now a derived view, not a source of truth —src/core.ts:125:The same sketch is duplicated in the body of #28 itself, in an even older form (no
projects/projectmembers at all). Both copies need the same amendment.app/CLAUDE.md:100makes this a precondition rather than a follow-up:The doc and
maindisagree today. So the ordering is: settle this issue, amenddocs/internal-analyzer-contract.mdand the sketch in #28's body, then write #28 code.The gap is already recorded in the code's own history
This is not a hypothetical. The closing comment on #70 says so explicitly, under "Scoped out, deliberately":
And #71's closing comment describes the policy surface as landing where the context would have:
That carrying is real,
src/file-inventory.ts:54-58:Meanwhile the de-facto context in
mainis a widening positional argument list —src/core.ts:136-156passes(resolvedCwd, stack, workspace, fileInventory),(resolvedCwd, workspace, fileInventory), and fortestinga six-argument call(resolvedCwd, stack, skipTests, srcRoots, workspace, fileInventory). Every runner takes the inventory as an optional trailing parameter (src/runners/secrets.ts:255,src/runners/flutter.ts:36,src/runners/structure.ts:34). Replacing that with a typed context is exactly what #28 is for, which is why the member list has to be right before it is frozen into 30+ call sites.The decision
D1 — Does
AnalyzerContextcarry the file universe? (primary)Recommendation: yes — add
files: FileInventoryand deleteignoreNames.files: FileInventory(recommended)ctx.files.file-inventory.ts. Matches whatcore.tsalready passes positionally.ctx.sourceFiles(),ctx.testFiles(),ctx.allFiles(opts)— closures bound to the inventory, inventory itself not exposed.ctx.files.policy, which #71 deliberately put on the inventory. Also cannot expressincludeIgnoredcases likeflutter's generated-Dart ratio without re-adding an escape hatch.core.tskeeps a bespoke argument list per runner, which is the thing the issue says it is removing.Rejecting B specifically: it is not wrong, but it duplicates a surface that shipped four days ago and has tests (
src/file-inventory.test.ts). If accessors are wanted later they can be added as sugar overctx.fileswithout another migration. Rejecting C: it makes #28 a rename rather than a contract.ignoreNames: string[]should be deleted, not kept alongside. It is a lossy projection ofctx.files.policyand, per #71's commit message, keeping two representations of one contract is what caused thefs-utilsdrift that #71 existed to remove. If a caller genuinely wants the flat list,policyIgnoreNames(ctx.files.policy)produces it.D2 — Is
policya separate member, or reached viactx.files.policy?Recommendation:
ctx.files.policyonly. Do not add a top-levelpolicymember.The inventory already owns exactly one policy and documents why (
src/file-inventory.ts:56-58, quoted above). A second reference creates a shape wherectx.policy !== ctx.files.policyis constructible, which is precisely the two-copies-of-one-contract failure mode.Counter-argument worth recording: out-of-process analyzers (#32) have to serialise the context over JSON/stdio, and
EffectiveScanPolicyis a small flat record (src/scan-policy.ts:43-54, nine fields) whileFileInventorycarries every walked path plusignoredFiles. A subprocess protocol will likely send the policy and a filtered file list rather than the whole inventory. That is an argument about the wire shape, not the in-process shape, and #32 is free to define its own projection. It is not a reason to split the member here — but it should be written into the doc so #32's author does not readfiles: FileInventoryas a serialisation requirement.D3 — Do
settings/effectiveSettingsstay as sketched? (unblocks #30)The sketch has both (
docs/internal-analyzer-contract.md:89-90). Recommendation: keep both, and specify thatsettingsis the raw user block andeffectiveSettingsis defaults-merged-and-validated, since the names do not say so.There is prior thinking on this that is not in the repo — see "Uncommitted work" below. That WIP resolves settings once per scan in
core.tsand delivers them viadetails.effectiveSettings/details.settingsWarningsper check plusmeta.analyzerSettingson the report, with a newEffectiveAnalyzerSettingstype. That approach needs noAnalyzerContextat all.So D3 has a sequencing sub-question the maintainer should answer: does #30 land on today's positional runners (as the WIP does), or does it wait for #28's context? Landing first is cheaper and unblocks the app surface sooner; waiting avoids writing a settings path that #28 immediately reworks. I lean toward landing #30 first provided the report-level
meta.analyzerSettingsshape is treated as the stable contract and the plumbing is understood to be temporary — but this is genuinely the maintainer's call, not mine.D4 — Does the context carry scope? (unblocks #58)
#58's first task is "Add analyzer scope metadata:
repo,project,file-graph,derived". The doc already half-specifies this in prose — line 109, "Repo-level analyzers receive the wholeprojectslist. Project-scoped analyzers receive oneprojectat a time" — and lifecycle step 2, line 140, "Select repo or project scope from the analyzer manifest". But noscopefield exists onAnalyzerManifest(docs/internal-analyzer-contract.md:66-74). The lifecycle instructs the engine to read a field the type does not have.Recommendation: add
scope: "repo" | "project" | "file-graph" | "derived"toAnalyzerManifest, and keep bothprojects?andproject?on the context as the sketch already has them.ProjectContextis already a shipped type (src/types.ts:50-63), so this costs nothing new. Rejected alternative: inferring scope from whether the analyzer readsctx.project— unknowable statically, and the engine has to decide iteration before calling the analyzer.D5 — Does the context carry analyzer identity, for tool-run provenance? (relates to app#52)
app#52 wants
ToolRunto record where a run came from, and proposes{ analyzerId, source: "builtin" | "plugin", pluginId?, version? }. Lifecycle step 7 (line 145) already says "Attach package-tagged tool provenance fromexec.ts", but nothing in the context tellsexec.tswhich analyzer is running.Recommendation: add
analyzerId: stringtoAnalyzerContextso the engine can stamporiginat the recording boundary rather than asking every analyzer to self-report. This is a one-word change here that makes app#52 implementable without touching each runner. Note app#52 lives invibecodeqa/appbut the field it wants is in@vibecodeqa/schema'sToolRun— that part is not this issue's to decide, and this issue only commits to making the CLI side supplyable.D6 — #58 vs #27: merge or parent? (sub-decision, as raised by the audit)
Verified: both are open and both are labelled
analyzer-platform, and #58 already lists#27under its Related section, so the overlap is acknowledged but not resolved.Recommendation: explicit parenting, not merging. Keep #58 as the implementation issue and rewrite #27's body to be the acceptance checkpoint for it — "verified by" rather than "also does". Reason for not merging: #27 carries the observable symptom a user reported, and folding it into #58 loses a separately closable proof that the symptom is gone. Reason for not leaving them as-is: two open issues describing one body of work will get picked up twice.
If the maintainer prefers merging, close #27 as duplicate of #58 after copying its symptom evidence into #58's acceptance criteria — not before.
What each choice unblocks
files: FileInventoryctx.filessketch #70 deferred; retires the residue of #70ctx.files.policysettings/effectiveSettingsmanifest.scope+project/projectsanalyzerIdKnown residues that are not resolved by this decision
Both are named in #70's closing comment and neither has an issue, so they should not be assumed in scope:
files.html({ deliverable: true })andfiles.config({ kind: "github-actions" })were sketched in Introduce FileInventory: classify scan files once and require runners to consume it #70 and have no implementation. The missingconfigquery is whylintandbest-practicesstill read.github/workflows/directly. If the context is to expose them, that needs its own issue.lint,types) let the tool discover files and filter output afterwards throughisIgnoredPath(). That is deliberate and documented indocs/exclusion-policy.mdunder "Walks that remain, and why". The context does not change it.Uncommitted work that should be read before deciding, and rescued
There is a stash in the local
clicheckout:It touches
docs/internal-analyzer-contract.md(+11 lines, a "Current implementation" block under Settings),src/types.ts(addsEffectiveAnalyzerSettingsandmeta.analyzerSettings),src/core.ts(+64/-8),src/config.ts, and tests. It is the prior thinking behind D3 and contains a concrete answer to "where do effective settings surface".It is backed up only to
/tmp/vcqa-wip-backup/(stash-0.patch, pluscli-uncommitted.patch,schema-uncommitted.patch,app-uncommitted-*.patch,cli-untracked.tgz). macOS prunes/tmpand the stash lives in one working copy on one machine; neither survives indefinitely. Whoever picks this up should move that backup somewhere durable, or land/drop the stash, before it evaporates.Acceptance criteria
files: FileInventoryand on a separatepolicymember.docs/internal-analyzer-contract.md'sAnalyzerContextblock matches the decision, and the document mentionsFileInventoryandEffectiveScanPolicyby name with a pointer todocs/exclusion-policy.md.ignoreNamesis dropped, the doc says what replaces it (policyIgnoreNames(ctx.files.policy)).AnalyzerManifestin the doc, so lifecycle step 2 no longer references a field that does not exist.Constraints
app/CLAUDE.md:100— "If these docs disagree, fix the docs before implementing the feature." The doc amendment is a precondition, not cleanup.cli/CLAUDE.md— trunk-based, commit straight tomain, no branches and no PRs. There is no PR to hold this decision in; the decision has to live in this issue.cli/CLAUDE.mdstack-gating rule still applies to whatever the context exposes: astack.framework === ...branch inside a generic analyzer is a rejected diff regardless of how the context is shaped.publish.yml, which then skips publishing unlesspackage.jsonversion changed. Amending the doc is safe; it just starts a job that no-ops.Related
ToolRunorigin)FileInventory,9764ca3), Define effective scan policy: ignore precedence, generated classification, and security overrides #71 (EffectiveScanPolicy,85278f7), ProjectContext contract for deterministic repo discovery #56 (ProjectContext) — all closed, all in 0.55.0